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é]. Для його розуміння необхідні певні попередні знання. Деякі з них можна отримати з документів, які я написав. У цьому випадку я наводжу посилання на них. Зрозуміло, що це лише рекомендація, і читач може використовувати свої улюблені документи.
- мова VB.net: [Вступ до мови VB.NET на прикладах (2004)];
- веб-програмування на VB.net: [Веб-розробка з використанням ASP.NET 1.1 (2004)];
- використання компонента IoC у Spring: [Spring IoC для .NET (2005)];
- документація Spring.net: [Spring.NET | Homepage ]
Цей документ повторює основну лінію документа, написаного для 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], яке повідомляє про будь-які помилки в роботі додатка

1.3. Загальна архітектура додатка
Ми хочемо створити додаток із такою трирівневою структурою:
![]() |
- три рівні є незалежними завдяки використанню інтерфейсів
- інтеграція різних рівнів здійснюється за допомогою Spring
- кожен рівень має окремий простір імен: web (рівень UI), domain (бізнес-рівень) та dao (рівень доступу до даних).
Додаток відповідатиме архітектурі MVC (Модель — Вигляд — Контролер). Якщо повернутися до наведеної вище схеми з шарами, архітектура MVC вписується в неї наступним чином:
![]() |
Обробка запиту клієнта відбувається за такими етапами:
- клієнт надсилає запит до контролера. Цим контролером тут буде сторінка .aspx, якій відведено особливу роль. Вона обробляє всі запити клієнтів. Це вхідні ворота додатка. Це «C» у MVC.
- контролер обробляє цей запит. Для цього йому може знадобитися допомога бізнес-шару, який у структурі MVC називається моделлю M.
- Контролер отримує відповідь від бізнес-шару. Запит клієнта було оброблено. Це може призвести до кількох можливих відповідей. Класичним прикладом є
- сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
- сторінка підтвердження в іншому випадку
- контролер обирає відповідь (= представлення), яку потрібно надіслати клієнту. Найчастіше це сторінка, що містить динамічні елементи. Контролер передає їх до представлення.
- Вигляд надсилається клієнту. Це «V» у MVC.
1.4. Модель
Тут ми розглядаємо «М» з MVC. Модель складається з таких елементів:
- бізнес-класи
- класи доступу до даних
- база даних
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);
первинний ключ, що однозначно ідентифікує товар | |
назва товару | |
її ціна | |
поточний запас | |
запас, при недосягненні якого необхідно оформити замовлення на поповнення запасів |
1.4.2. Простори імен моделі
Модель M тут представлена у вигляді двох просторів імен:
- istia.st.articles.dao: містить класи доступу до даних шару [dao]
- istia.st.articles.domain: містить бізнес-класи рівня [domain]
Кожен із цих просторів імен буде згенеровано у власному файлі «assembly»:
contenu | rôle | |
- [IArticlesDao]: інтерфейс доступу до шару [dao]. Це єдиний інтерфейс, який бачить рівень [domain]. Інших він не бачить. - [Article]: клас, що визначає товар - [ArticlesDaoArrayList]: клас реалізації інтерфейсу [IArticlesDao] за допомогою класу [ArrayList] | шар доступу до даних - повністю знаходиться в шарі [dao] трирівневої архітектури веб-додатку | |
- [IArticlesDomain]: інтерфейс доступу до шару [domain]. Це єдиний інтерфейс, який бачить веб-шар. Інших він не бачить. - [AchatsArticles]: клас, що реалізує [IArticlesDomain] - [Achat]: клас, що представляє покупку клієнта - [Panier]: клас, що представляє сукупність покупок клієнта | представляє модель покупок у Інтернеті — повністю розміщений у шарі [domain] архітектури 3-рівнева архітектура веб-додатку |
1.4.3. Рівень [dao]
Рівень [dao] містить такі елементи:
-
[IArticlesDao]: інтерфейс доступу до шару [dao]
-
[Article]: клас, що визначає статтю
-
[ArticlesDaoArrayList]: клас реалізації інтерфейсу [IArticlesDao] за допомогою класу [ArrayList]
Структура проєкту [Visual Studio] на рівні [dao] є такою:

Коментарі:
- проект [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
Цей клас надає:
- конструктор, що дозволяє задати 5 параметрів статті: [id, nom, prix, stockactuel, stockminimum]
- публічні властивості, що дозволяють читати та записувати ці 5 елементів інформації.
- перевірку даних, введених у товар. У разі помилкових даних генерується виняток.
- метод 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
Роль різних методів інтерфейсу така:
повертає всі записи з джерела даних | |
очищає джерело даних | |
повертає об’єкт [Article], ідентифікований за його первинним ключем | |
дозволяє додати товар до джерела даних | |
дозволяє змінити товар у джерелі даних | |
дозволяє видалити товар із джерела даних | |
дозволяє змінити залишок товару у джерелі даних |
Інтерфейс надає клієнтським програмам певну кількість методів, визначених виключно їхніми сигнатурами. Він не визначає, як ці методи будуть фактично реалізовані. Це забезпечує гнучкість у роботі додатка. Клієнтська програма звертається до інтерфейсу, а не до його конкретної реалізації.
![]() |
Вибір конкретної реалізації здійснюватиметься за допомогою конфігураційного файлу 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 надає ті самі можливості для модульного тестування:

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

Коментарі:
- проект [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]. Тому там міститься така інструкція імпорту:
Простір імен [NUnit.Framework] знаходиться в «асемблі» [nunit.framework.dll], яку потрібно додати до посилань проекту:
![]() | ![]() |
Асемблі [nunit.framework.dll] має бути у запропонованому списку, якщо було виконано встановлення [Nunit]. Достатньо двічі клацнути на асемблі, щоб додати його до проекту:

Тестовий клас [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 і виконуємо тести:

У лівому вікні відображається список протестованих методів. Колір крапки перед назвою кожного методу вказує на успішне виконання (зелений) або невдале виконання (червоний) методу. Читач, який переглядає цей документ на екрані, побачить, що всі тести пройшли успішно. Далі ми вважатимемо, що маємо робочий шар [dao].
1.4.4. Шар [domain]
Рівень [domain] містить такі елементи:
-
[IArticlesDomain]: інтерфейс доступу до шару [domain]
-
[Achat]: клас, що визначає покупку
-
[Panier]: клас, що визначає кошик покупок
-
[AchatsArticles]: клас реалізації інтерфейсу [IArticlesDomain]
Структура рішення [Visual Studio] на рівні [domain] є такою:

Коментарі:
- проект [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
повертає список об’єктів [Article] із пов’язаного джерела даних | |
повертає об’єкт [Article], ідентифікований за [idArticle] | |
підтверджує кошик клієнта, зменшуючи запаси придбаних товарів на кількість, що була придбана — може завершитися невдачею, якщо запасів недостатньо | |
повертає список помилок, що виникли — порожній, якщо помилок немає |
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] має такі властивості та методи:
придбаний товар | |
кількість, що була придбана | |
сума покупки | |
ідентифікаційний рядок об’єкта |
- вона має конструктор, що дозволяє ініціалізувати властивості [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] має такі властивості та методи:
список покупок клієнта — список об’єктів типу [Achat] | |
додає покупку до списку покупок | |
видаляє покупку товару idAchat | |
загальна сума покупок у кошику | |
повертає ідентифікаційний рядок кошика |
- метод [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]. Він має два приватні поля:
об’єкт доступу до даних | |
список можливих помилок. Доступ до нього здійснюється через публічну властивість [erreurs] |
- щоб створити екземпляр класу, потрібно вказати об’єкт, що забезпечує доступ до даних:
- методи [getAllArticles] та [getArticleById] базуються на однойменних методах шару [dao]
- метод [acheter] підтверджує покупку кошика. Це підтвердження полягає просто у зменшенні запасів придбаних товарів. Придбати товар можна лише за умови наявності його на складі. Якщо товару на складі немає, покупка відхиляється: товар залишається у кошику, а у списку [erreurs] фіксується помилка. Підтверджена покупка вилучається з кошика, а запас відповідного товару зменшується на кількість, що була придбана.
1.4.4.5. Створення збірки шару [domain]
Проєкт Visual Studio налаштовано на створення збірки [webarticles-domain.dll]. Вона створюється у папці [bin] проєкту:
![]() | ![]() |
1.4.4.6. Тести NUnit для шару [domain]
Структура тестового проєкту Visual Studio є такою:

Коментарі:
- проект [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 і виконуємо тести:

Користувач, який переглядає цей документ на екрані, побачить, що всі тести пройшли успішно. Отже, вважатимемо, що ми маємо робочий шар [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 веб-додатку буде такою:
![]() |
бізнес-класи [domain], класи доступу до даних [dao] та джерело даних | |
сторінки ASPX | |
усі запити клієнтів HTTP проходять через два наступні контролери: global.asax: обробляє події, пов’язані з початковим запуском додатка main.aspx: обробляє запити кожного клієнта окремо |
1.5.1. Види
Види відповідають тим, що були наведені на початку документа:
liste.aspx | Види зібрані у папці [vues] програми ![]() | |
infos.aspx | ||
panier.aspx | ||
paniervide.aspx | ||
erreurs.aspx |
1.5.2. Контролери
Як уже зазначалося, контролер складатиметься з двох елементів:
- [global.asax,global.asax.vb]: використовується переважно для ініціалізації додатка та завантаження в його контекст усіх даних, які мають бути спільними для різних клієнтів
- [main.aspx, main.aspx.vb] — власне контролер, який обробляє запити HTTP від клієнтів.
Різні запити клієнтів надсилатимуться до контролера [main.aspx] і міститимуть параметр під назвою [action], що визначає дію, яку вимагає клієнт:
запит | значення | дія контролера | можливі відповіді |
клієнт хоче отримати список товарів | - запитує перелік товарів у бізнес-шарі бізнес-шару | - [LISTE] - [ERREURS] | |
клієнт запитує інформацію про один із товарів, що відображаються у вікні [LISTE] | - запитує товар у бізнес-шарі | - [INFOS] - [ERREURS] | |
клієнт купує товар | - надсилає запит на товар до бізнес-шару і додає її до кошика клієнта | - [INFOS], якщо сталася помилка з кількістю - [LISTE], якщо помилки немає | |
клієнт хоче видалити товар зі свого кошика | - отримує кошик із сесії і змінює його | - [PANIER] - [PANIERVIDE] - [ERREURS] | |
клієнт хоче переглянути свій кошик | - отримує кошик із сесії | - [PANIER] - [PANIERVIDE] - [ERREURS] | |
клієнт завершив покупки і переходить до етапу оплати | - оновлює в базі даних запаси придбаних товарів - очищає кошик клієнта від товарів , покупка яких була підтверджена | - [LISTE] - [ERREURS] |
1.5.3. Налаштування додатка
Ми намагатимемося налаштувати додаток так, щоб він був максимально гнучким щодо таких змін, як:
- зміна URL різних представлень
- зміна класів, що реалізують інтерфейси [IArticlesDao] та [IArticlesDomain]
- зміна 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], яке відображає список помилок:

Вигляд [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>
Тег
використовується для повторення шаблону HTML у різних елементах джерела даних. Її різні елементи такі:
шаблон HTML, який має відображатися перед тим, як будуть показані елементи джерела даних | |
мотив HTML, який повторюється для кожного елемента джерела даних. Вираз [<%# Container.DataItem %>] використовується для відображення значення поточного елемента джерела даних | |
шаблон 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, що використовуються додатком, є такими:
об’єднує класи рівня доступу до даних | |
об’єднує класи бізнес-шару | |
містить класи Spring, які дозволяють нам інтегрувати веб-шар, доменний шар та шар DAO | |
класи журналів — використовуються 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> для відображення таблиці опцій меню:
![]() |
n° | type | nom | rôle |
1 | повторювач | rptMenu джерело даних: масив словників з двома ключами: href, посилання | відобразити опції меню |
Код для відображення сторінки буде таким:
Коментарі:
- компонент [repeater] визначено в рядках 6–14
- кожен елемент джерела даних, пов'язаний з ретранслятором, є словником з двома ключами: href — рядок 9 та посилання — рядок 10
Відповідний контрольний код буде таким:
Коментарі:
- компонент типу [EnteteWebArticles] має загальнодоступну властивість [actions], доступну лише для запису — рядок 7
- це властивість дозволяє пов'язати з компонентом <asp:repeater> під назвою [rptMenu] — рядок 10 — масив опцій, обчислений контролером додатка — рядки 11–12.
Інші подання додатка використовуватимуть заголовок, визначений [entete.ascx]. Наприклад, сторінка [erreurs.aspx] включить заголовок за допомогою такого коду:
Коментарі:
- У рядку 1 вказано, що тег <WA:entete> має бути пов’язаний із компонентом, визначеним у файлі [entete.ascx]. Атрибути [TagPrefix] та [TagName] можна задавати довільно.
- Після цього вставлення компонента в код оформлення сторінки здійснюється за допомогою рядка 9. Під час виконання цей тег призведе до включення в код сторінки ASPX, яка його містить, коду сторінки [entete.ascx]. Код управління [erreurs.aspx.vb] подбає про ініціалізацію цього компонента. Це можна зробити наступним чином:
Коментарі:
- у рядку 6 створюється об’єкт типу [EnteteWebArticles], що відповідає типу створеного компонента
- у рядку 11 ініціалізується властивість [actions] цього об’єкта
1.5.6.2. Вигляд [liste.aspx]
1.5.6.2.1. Introduction
Цей вигляд відображає список товарів, доступних для продажу:
![]() | ![]() |
Вона відображається після запиту /main?action=liste або /main?action=validationpanier. Елементи запиту контролера такі:
об’єкт Hashtable() — масив опцій меню | |
ArrayList об’єктів типу [Article] | |
об'єкт String — повідомлення, яке відображається внизу сторінки |
Кожне посилання [Infos] з масиву HTML статей має URL у формі [?action=infos&id=ID], де ID — це поле idвідображеного елемента.
1.5.6.2.2. Компоненти сторінки
![]() |
n° | type | nom | rôle |
компонент користувача | заголовок | показати заголовок | |
DataGrid | DataGridArticles 3 — пов'язаний стовпець: заголовок: Ім'я, поле: ім'я 4 — пов'язаний стовпець: заголовок: Ціна, поле: ціна 5 — гіпертекстовий стовпець: текст: Інформація, поле URL: id, формат URL: /webarticles/main.aspx?action=infos&id={0} | показати товари, що продаються | |
мітка | lblMessage | показати повідомлення |
Нагадаємо, як налаштувати ці властивості:
- у Visual Studio виберіть [DataGrid], щоб отримати доступ до його вікна властивостей:

- використовуємо посилання [Mise en forme automatique], наведене вище, щоб налаштувати форму таблиці, що відображається
- а посилання [Générateur de propriétés] — для налаштування його вмісту
1.5.6.2.3. Код оформлення [liste.aspx]
Коментарі:
- рядок 9 визначає заголовок сторінки
- рядки 12–24 визначають характеристики [DataGrid]
- рядок 26 визначає мітку [lblMessage]
1.5.6.2.4. Код контролера [liste.aspx.vb]
Коментарі:
- компоненти сторінки наводяться у рядках 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
Цей вигляд відображає інформацію про товар, а також дозволяє його придбати:

Вона відображається після запиту /main?action=infos&id=ID або запиту /main?action=achat&id=ID, коли кількість, що купується, є неправильною. Елементи запиту контролера такі:
об’єкт Hashtable() — масив опцій меню | |
об’єкт типу [Article] — товар, який потрібно відобразити | |
об'єкт String — повідомлення, яке слід відобразити у разі помилки щодо кількості | |
об'єкт String — значення, яке потрібно відобразити у полі введення [Qte] |
Поля [msg] та [qte] використовуються у разі помилки введення кількості:

Ця сторінка містить форму, яка відправляється за допомогою кнопки [Acheter]. Цільовим полем URL для поля POST є [?action=achat&id=ID], де ID — це ідентифікатор придбаного товару.
1.5.6.3.2. Компоненти сторінки
![]() |
n° | type | nom | rôle |
компонент користувача | заголовок | показати заголовок | |
літерал | litID | показати номер статті | |
DataGrid | DataGridArticle 3 — пов'язаний стовпець: заголовок: Ім'я, поле: ім'я 4 — пов'язаний стовпець: заголовок: Ціна, поле: ціна 5 — пов'язаний стовпець: заголовок: «Поточний запас», поле: stockActuel 6 — пов'язаний стовпець: заголовок: «Мінімальний запас», поле: stockMinimum | переглянути товар | |
HTML Надіслати | Відправити форму | ||
HTML Ввести runat=server | txtQte | ввести кількість, що купується | |
label | lblMsgQte | можливе повідомлення про помилку |
1.5.6.3.3. Код представлення [infos.aspx]
Коментарі:
- заголовок включено на сторінку — рядок 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]
Коментарі:
- компоненти сторінки визначено у рядках 10–14
- клас визначає публічну властивість [strAction], яка слугує для визначення цільового елемента POST у формі — рядки 17–25
- стаття, яку потрібно відобразити, отримується з контексту додатка — рядок 30
- таблиця опцій меню заголовка береться з контексту для ініціалізації компонента [entete] сторінки — рядок 32
- у рядках 33–39 компонент [DataGridArticle] пов’язується з джерелом даних типу [ArrayList], що містить лише товар, отриманий у рядку 30
- компоненти [lblMsgQte, txtQte] ініціалізуються за допомогою інформації, взятої з контексту — рядки 42–45
- властивість [straction] також ініціалізується з використанням інформації, взятої з контексту — рядок 47. Ця змінна використовується для генерації атрибута [action] форми HTML, що міститься на сторінці:
1.5.6.4. Вигляд [panier.aspx]
1.5.6.4.1. Introduction
Цей вигляд відображає вміст кошика:

Вона відображається після запиту /main?action=panier або /main?action=retirerachat&id=ID. Елементи запиту контролера такі:
об’єкт Hashtable() — масив опцій меню | |
об’єкт типу [Panier] — кошик, який потрібно відобразити |
Кожне посилання [Retirer] у масиві HTML покупок у кошику має URL у формі [?action=retirerachat&id=ID], де ID — це поле [id] товару, який потрібно видалити з кошика.
1.5.6.4.2. Компоненти сторінки
![]() |
n° | type | nom | rôle |
компонент користувача | заголовок | показати заголовок | |
DataGrid | DataGridAchats 3 — пов'язаний стовпець — заголовок: «Товар», поле: «назва» 4 — пов'язаний стовпець — заголовок: Кількість, поле: кількість 5 — пов'язаний стовпець — заголовок: Ціна, поле: ціна 6 — пов'язаний стовпець — заголовок: Сума, поле: сума, форматування {0:C} 7 — гіпертекстовий стовпець — Текст: Вилучити, URL: id, формат URL: /webarticles/main.aspx?action=retirerachat&id={0} | відобразити список придбаних товарів | |
мітка | lblTotal | показати суму до сплати |
1.5.6.4.3. Код відображення [panier.aspx]
Коментарі
- рядок 9 містить заголовок
- у рядках 12–27 визначено компонент [DataGridAchats]
- у рядку 29 визначено компонент [lblTotal]
1.5.6.4.4. Контрольний код [panier.aspx.vb]
Коментарі:
- компоненти сторінки оголошені у рядках 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
Цей вигляд відображає інформацію про те, що кошик порожній:

Вона відображається після запиту /main?action=panier або /main?action=retirerachat&id=ID. Елементи запиту контролера такі:
об’єкт Hashtable() — масив опцій меню |
1.5.6.5.2. Компоненти сторінки
![]() |
n° | type | nom | rôle |
компонент користувача | заголовок | показати заголовок |
1.5.6.5.3. Код представлення [paniervide.aspx]
Коментарі:
- заголовок включено у рядок 9
1.5.6.5.4. Контрольний код [paniervide.aspx.vb]
Коментарі:
- ми просто ініціалізуємо єдиний динамічний компонент сторінки — рядок 10
1.5.6.6. Вигляд [erreurs.aspx]
1.5.6.6.1. Introduction
Цей екран відображається у разі помилок:

Вона відображається після будь-якого запиту, що призвів до помилки, за винятком дії купівлі з неправильною кількістю, яка обробляється у вікні [INFOS]. Елементи запиту контролера такі:
об’єкт Hashtable() — масив опцій меню | |
ArrayList об’єктів [String], що представляють повідомлення про помилки, які потрібно відобразити |
1.5.6.6.2. Компоненти сторінки
![]() |
n° | type | nom | rôle |
компонент користувача | заголовок | показати заголовок | |
повторювач | rptErreurs | показати список помилок |
1.5.6.6.3. Код представлення [erreurs.aspx]
Коментарі:
- заголовок визначено у рядку 9
- компонент [rptErreurs] визначено у рядках 13–19. Його вміст походить із джерела даних типу [ArrayList], що складається з об’єктів [String].
1.5.6.6.4. Контрольний код [erreurs.aspx.vb]
Коментарі:
- компонент [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], яка виконується щоразу, коли з’являється новий клієнт. У цій процедурі для клієнта створюється порожній кошик. Цей кошик зберігатиметься протягом усіх запитів цього конкретного клієнта. Код може виглядати так:
Коментарі:
- очікувані параметри в [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] є дійсним, запит клієнта передається для обробки до процедури, специфічної для даної дії:
метод | запит | обробка | можливі відповіді |
GET /main?action=liste | - запитати список статей у бізнес-класі - відобразити | [LISTE] або [ERREURS] | |
GET /main?action=infos&id=ID | - запросити статтю з id=ID у бізнес-класу - відобразити його | [INFOS] або [ERREURS] | |
POST /main?action=achat&id=ID - кількість, що була придбана, входить до параметрів, що передаються | - запитати товар з id=ID у бізнес-класу - додати його до кошика в сесії клієнта | [LISTE] або [INFOS] або [ERREURS] | |
GET /main?action=retirerachat&id=ID | - видалити товар з id=ID зі списку покупок у кошику сесії клієнта | [PANIER] | |
GET /main?action=panier | - відобразити кошик сесії клієнта | [PANIER] або [PANIERVIDE] | |
GET /main?action=validationpanier | - зменшити в базі даних залишки всіх товарів , що знаходяться у кошику сесії клієнта | [LISTE] або [ERREURS] |
Скелет контролера [main.aspx.vb] може виглядати наступним чином:
Коментарі:
- клас має два приватні поля, які будуть спільними для методів — рядки 15–16:
- articlesDomain: синглтон для доступу до шару [domain]
- options: масив словників опцій меню
- процедура [Page_Load]:
- ініціалізує два приватні поля класу
- отримає параметр [action] із запиту та запустить метод обробки цієї дії.
1.5.7.3. Метод [Page_Load]
Ця подія відбувається першою на сторінці. Код такий:
Коментарі:
- під час кожного завантаження сторінки перевіряється, чи правильно пройшла ініціалізація додатка, виконана методом [global.asax].
- Для цього з контексту додатка отримуємо список помилок, розміщений там [global.asax] — рядок 4
- якщо цей список не порожній, відображається представлення [ERREURS] — рядки 6–10
- отримуємо синглтон [articlesDomain], розміщений [global.asax] у контексті додатка, і зберігаємо його у приватному полі [articlesDomain], щоб він був доступний для різних методів класу — рядок 12
- виконуємо аналогічну операцію з масивом опцій меню — рядок 14
- з запиту отримуємо параметр [action] — рядок 16
- виконуємо метод, що відповідає запитуваній дії. Непередбачена дія розглядається як дія [liste] — рядки 16–36
1.5.7.4. Обробка дії [liste]
Йдеться про відображення списку товарів:

Код виглядає наступним чином:
Коментарі:
- можливі помилки записуються у [ArrayList] — рядок 4
- запит на список статей надсилається до синглтона [articlesDomain] — рядки 5–12
- якщо виникли помилки, надсилається представлення [ERREURS] — рядки 13–19
- інакше надсилається представлення [LISTE] — рядки 20–24
1.5.7.5. Обробка дії [infos]
Клієнт запитав інформацію про певний товар:

Код такий:
Коментарі:
- можливі помилки записуються у [ArrayList] — рядок 4
- ідентифікатор запитуваного товару отримується із запиту — рядок 6
- цей ідентифікатор перевіряється. Він має бути присутнім і бути цілим числом. Якщо це не так, надсилається представлення [ERREURS] із відповідним повідомленням про помилку — рядки 7–27
- після перевірки ідентифікатора товар запитується у синглтоні [articlesDomain]. Якщо виникає виняток, надсилається представлення [ERREURS] — рядки 29–39
- якщо товар не знайдено, надсилається представлення [ERREURS] — рядки 41–48
- якщо товар знайдено, його додають до сесії користувача, а потім відображають у поданні [INFOS] — рядки 50–56
1.5.7.6. Обробка дії [achat]
Клієнт придбав товар, що відображається у вікні [INFOS].

Код такий:
Коментарі:
- товар, доданий до сесії, витягується — рядок 5
- якщо його немає (сесія могла закінчитися), відображається вид [LISTE] — рядки 7–10
- кількість, що була придбана, отримується із запиту — рядок 12
- перевіряється її дійсність — рядки 13–29
- у разі недійсності, залежно від ситуації, надсилається подання [LISTE] — рядок 16 або подання [INFOS] — рядки 24–28
- якщо все в порядку, покупка записується в кошик — рядки 31–32
- потім надсилається звіт [LISTE] — рядок 34
1.5.7.7. Обробка дії [panier]
Клієнт здійснив кілька покупок і просить переглянути кошик:

Код виглядає так:
Коментарі:
- кошик отримуємо з сесії — рядок 4. Тут не перевіряємо, чи дійсно щось отримано. Це слід зробити, оскільки сесія могла закінчитися.
- якщо кошик порожній, надсилається сторінка [PANIERVIDE] — рядки 6–10
- в іншому випадку надсилається вид [PANIER] — рядки 11–14
1.5.7.8. Обробка дії [retirerachat]
Клієнт хоче видалити товар із кошика:

Код такий:
Коментарі:
- кошик отримуємо з сесії — рядок 4. Тут не перевіряємо, чи дійсно щось отримано. Це слід зробити, оскільки сесія могла закінчитися.
- У запиті отримуємо ідентифікатор [id] товару, який потрібно видалити — рядок 8.
- Відповідний товар видаляється з кошика — рядок 10
- Тут не перевіряється дійсність ідентифікатора придбаного товару. Якщо він має недійсний тип, виникне виняток, який буде оброблено в рядках 11–13. Якщо він дійсний, але не існує, метод [panier.enlever] (рядок 10) нічого не робить.
- виводиться новий кошик — рядок 16
1.5.7.9. Обробка дії [validerpanier]
Клієнт хоче підтвердити свій кошик:

Код такий:
Коментарі:
- кошик отримуємо з сесії — рядок 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 для додатків середнього розміру.































