1. Część 1
Plik PDF z tego dokumentu jest dostępny jako |TUTAJ|.
Przykłady z dokumentu są dostępne pod numerem |TUTAJ|.
1.1. Introduction
Cele artykułu:
- napisanie trójwarstwowej aplikacji internetowej [interface utilisateur, métier, accès aux données]
- skonfigurowanie aplikacji przy użyciu Springa IOC
- napisać różne wersje poprzez zmianę implementacji jednej lub kilku z trzech warstw.
Wykorzystane narzędzia:
- Visual Studio.net do tworzenia aplikacji – patrz załącznik, punkt 3.1;
- Serwer WWW Cassini do uruchamiania – patrz załącznik, punkt 3.2;
- Nunit do testów jednostkowych – patrz załącznik, punkt 3.4;
- Spring do integracji i konfiguracji warstw aplikacji internetowej – patrz załącznik, punkt 3.3;
W skali poziomów początkujący–średniozaawansowany–zaawansowany niniejszy dokument plasuje się w części [intermédiaire-avancé]. Jego zrozumienie wymaga spełnienia różnych warunków wstępnych. Niektóre z nich można poznać w dokumentach, które sam napisałem. W takim przypadku je cytuję. Oczywiste jest, że jest to jedynie sugestia i że czytelnik może korzystać ze swoich ulubionych dokumentów.
- język VB.net: [Wprowadzenie do języka VB.NET na przykładach (2004)];
- programowanie stron internetowych w języku VB.net: [Tworzenie aplikacji internetowych z wykorzystaniem ASP.NET 1.1 (2004)];
- korzystanie z komponentu IoC w Spring: [Spring IoC dla .NET (2005)];
- dokumentacja Spring.net: [Spring.NET | Homepage ]
Niniejszy dokument nawiązuje do dokumentu poświęconego Javie [Architectures à 3 couches et architectures MVC avec Struts, Spring et Java ]. W VB.NET tworzymy trójwarstwową aplikację internetową MVC napisaną w Javie. Chcemy tutaj podkreślić, że platformy programistyczne Java i .NET są na tyle do siebie zbliżone, że umiejętności zdobyte w jednej z tych dziedzin można wykorzystać w drugiej.
Wydaje się, że nie istnieje powszechnie uznane rozwiązanie programistyczne MVC ASP.NET. Poniższe rozwiązanie opiera się na metodzie przedstawionej w dokumencie [Tworzenie aplikacji internetowych z wykorzystaniem ASP.NET 1.1 (2004)]. Chociaż ma ona tę zaletę, że wykorzystuje koncepcje powszechnie stosowane w programowaniu J2EE, należy ją jednak traktować wyłącznie jako to, czym jest – c.a.d – jedną z wielu metod programowania MVC. Gdy tylko metoda tworzenia oprogramowania MVC w ASP.NET zyska powszechną akceptację, należy ją wówczas przyjąć. Wersja .NET platformy Spring, obecnie w fazie rozwoju, może okazać się pierwszym takim rozwiązaniem.
1.2. Aplikacja webarticles
Przedstawiamy tutaj elementy uproszczonej aplikacji internetowej do handlu elektronicznego. Umożliwi ona użytkownikom sieci:
- przeglądanie listy produktów pochodzących z bazy danych
- dodawanie niektórych z nich do koszyka
- zatwierdzenie koszyka. Jedynym skutkiem tego zatwierdzenia będzie aktualizacja stanów magazynowych zakupionych artykułów w bazie danych.
Użytkownikowi będą prezentowane następujące widoki:
- widok „LISTE”, zawierający listę produktów w sprzedaży ![]() | - widok „[INFOS]”, który zawiera dodatkowe informacje o produkcie: ![]() |
- widoki [PANIER] i [PANIERVIDE], które przedstawiają zawartość koszyka klienta
![]() | ![]() |
- widok [ERREURS], który sygnalizuje wszelkie błędy aplikacji

1.3. Ogólna architektura aplikacji
Chcemy stworzyć aplikację o następującej strukturze trójwarstwowej:
![]() |
- trzy warstwy są od siebie niezależne dzięki zastosowaniu interfejsów
- integracja poszczególnych warstw odbywa się za pomocą biblioteki Spring
- każda warstwa posiada oddzielną przestrzeń nazw: web (warstwa UI), domain (warstwa biznesowa) oraz dao (warstwa dostępu do danych).
Aplikacja będzie zgodna z architekturą MVC (Model – Widok – Kontroler). Jeśli powrócimy do powyższego schematu warstwowego, architektura MVC wpisuje się w niego w następujący sposób:
![]() |
Przetwarzanie żądania klienta przebiega zgodnie z następującymi etapami:
- klient wysyła żądanie do kontrolera. Kontrolerem tym będzie w tym przypadku strona .aspx, której przypisano szczególną rolę. Przechodzą przez nią wszystkie żądania klientów. Stanowi ona bramę wejściową do aplikacji. Jest to litera C w skrócie MVC.
- kontroler przetwarza to żądanie. W tym celu może potrzebować pomocy warstwy biznesowej, zwanej modelem M w strukturze MVC.
- Kontroler otrzymuje odpowiedź od warstwy biznesowej. Żądanie klienta zostało przetworzone. Może to skutkować kilkoma możliwymi odpowiedziami. Klasycznym przykładem jest
- strona błędów, jeśli żądania nie udało się poprawnie przetworzyć
- w przeciwnym razie strona potwierdzenia
- kontroler wybiera odpowiedź (= widok), którą ma wysłać do klienta. Najczęściej jest to strona zawierająca elementy dynamiczne. Kontroler dostarcza je do widoku.
- Widok jest wysyłany do klienta. Jest to „V” w skrócie MVC.
1.4. Model
W tym miejscu analizujemy literę M z kodu MVC. Model składa się tutaj z następujących elementów:
- klasy biznesowe
- klasy dostępu do danych
- baza danych
1.4.1. Baza danych
Baza danych zawiera tylko jedną tabelę o nazwie ARTICLES. Została ona wygenerowana za pomocą następujących poleceń 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
);
/* ograniczenia */
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);
/* klucz główny */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
klucz podstawowy jednoznacznie identyfikujący artykuł | |
nazwa artykułu | |
jego cena | |
aktualny stan magazynowy | |
poziom zapasów, poniżej którego należy złożyć zamówienie na uzupełnienie zapasów |
1.4.2. Przestrzenie nazw modelu
Szablon M jest tutaj przedstawiony w postaci dwóch przestrzeni nazw:
- istia.st.articles.dao: zawiera klasy dostępu do danych warstwy [dao]
- istia.st.articles.domain: zawiera klasy biznesowe warstwy [domain]
Każda z tych przestrzeni nazw zostanie wygenerowana w ramach własnego pliku „assembly”:
contenu | rôle | |
- [IArticlesDao]: interfejs dostępu do warstwy [dao]. Jest to jedyny interfejs widoczny dla warstwy [domain]. Nie widzi ona żadnego innego. - [Article]: klasa definiująca artykuł - [ArticlesDaoArrayList]: klasa implementująca interfejsu [IArticlesDao] z klasą [ArrayList] | warstwy dostępu do danych - znajduje się w całości w warstwie [dao] w architekturze trójwarstwowej aplikacji internetowej | |
- [IArticlesDomain]: interfejs dostępu do warstwy [domain]. Jest to jedyny interfejs widoczny dla warstwy internetowej. Nie widzi ona żadnego innego. - [AchatsArticles]: klasa implementująca [IArticlesDomain] - [Achat]: klasa reprezentująca zakup klienta - [Panier]: klasa reprezentująca zbiór zakupów klienta | przedstawia model zakupów w – znajduje się w całości w warstwie [domain] architektury trójwarstwowej architektury aplikacji internetowej |
1.4.3. Warstwa [dao]
Warstwa [dao] zawiera następujące elementy:
-
[IArticlesDao]: interfejs dostępu do warstwy [dao]
-
[Article]: klasa definiująca artykuł
-
[ArticlesDaoArrayList]: klasa implementująca interfejs [IArticlesDao] z wykorzystaniem klasy [ArrayList]
Struktura projektu [Visual Studio] warstwy [dao] jest następująca:

Uwagi:
- projekt [dao] jest typu [bibliothèque de classes]
- klasy zostały umieszczone w drzewie katalogów, którego katalogiem głównym jest folder [istia]. Wszystkie znajdują się w przestrzeni nazw [istia.st.articles.dao].
1.4.3.1. Klasa [Article]
Klasa definiująca artykuł ma następujący wygląd:
Imports System
Namespace istia.st.articles.dao
Public Class Article
' pola prywatne
Private _id As Integer
Private _nom As String
Private _prix As Double
Private _stockactuel As Integer
Private _stockminimum As Integer
' identyfikator artykułu
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
' nazwa artykułu
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
' cena artykułu
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
' aktualny stan magazynowy artykułu
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
' minimalny stan magazynowy artykułu
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
' domyślny producent
Public Sub New()
End Sub
' producent z właściwościami
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
' metoda identyfikacji artykułu
Public Overrides Function ToString() As String
Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
End Function
End Class
End Namespace
Ta klasa oferuje:
- konstruktor umożliwiający ustawienie 5 informacji dotyczących artykułu: [id, nom, prix, stockactuel, stockminimum]
- publiczne właściwości umożliwiające odczyt i zapis tych 5 informacji.
- weryfikację danych wprowadzonych do artykułu. W przypadku błędnych danych generowany jest wyjątek.
- metodę toString, która pozwala uzyskać wartość artykułu w postaci ciągu znaków. Jest to często przydatne podczas debugowania aplikacji.
1.4.3.2. Interfejs [IArticlesDao]
Interfejs [IArticlesDao] jest zdefiniowany w następujący sposób:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Interface IArticlesDao
' lista wszystkich artykułów
Function getAllArticles() As IList
' dodaje artykuł
Function ajouteArticle(ByVal unArticle As Article) As Integer
' usuwa artykuł
Function supprimeArticle(ByVal idArticle As Integer) As Integer
' modyfikuje artykuł
Function modifieArticle(ByVal unArticle As Article) As Integer
' wyszukuje artykuł
Function getArticleById(ByVal idArticle As Integer) As Article
' usuwa wszystkie artykuły
Sub clearAllArticles()
' zmienia stan magazynowy artykułu
Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
End Interface
End Namespace
Rola poszczególnych metod interfejsu jest następująca:
zwraca wszystkie artykuły z źródła danych | |
opróżnia źródło danych | |
zwraca obiekt [Article] identyfikowany przez jego klucz główny | |
umożliwia dodanie pozycji do źródła danych | |
umożliwia modyfikację pozycji w źródle danych | |
umożliwia usunięcie pozycji ze źródła danych | |
umożliwia zmianę stanu magazynowego pozycji w źródle danych |
Interfejs udostępnia programom klienckim pewną liczbę metod zdefiniowanych wyłącznie poprzez ich sygnatury. Nie zajmuje się tym, w jaki sposób metody te zostaną faktycznie zaimplementowane. Zapewnia to elastyczność aplikacji. Program kliencki wywołuje interfejs, a nie konkretną implementację tego interfejsu.
![]() |
Wybór konkretnej implementacji nastąpi za pomocą pliku konfiguracyjnego Spring. Aby pokazać, że do testowania aplikacji internetowej liczy się wyłącznie interfejs dostępu do danych, a nie jego klasa implementacyjna, najpierw zaimplementujemy źródło danych za pomocą prostego obiektu [ArrayList]. W dalszej części przedstawimy rozwiązanie oparte na SGBD.
1.4.3.3. Klasa implementacyjna [ArticlesDaoArrayList]
Klasa implementacyjna [ArticlesDaoArrayList] jest zdefiniowana w następujący sposób:
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
' domyślny producent
Public Sub New()
' produkujemy kilka artykułów
For i As Integer = 1 To nbArticles
articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
Next
End Sub
' lista wszystkich artykułów
Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
' zwraca listę artykułów
SyncLock Me
Return articles
End SyncLock
End Function
' usunięcie wszystkich artykułów
Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
' czyści się listę artykułów
SyncLock Me
articles.Clear()
End SyncLock
End Sub
' pobieranie artykułu o określonym kluczu
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
' wyszukiwanie pozycji na liście
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
' dodaj pozycję do listy pozycji
Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
' dodajemy pozycję do listy pozycji
SyncLock Me
' sprawdzamy, czy artykuł nie istnieje już na liście
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
' dodaje się pozycję
articles.Add(unArticle)
' zwracamy wynik
Return 1
End SyncLock
End Function
' modyfikacja artykułu
Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
' modyfikuje się artykuł
SyncLock Me
' sprawdza się, czy istnieje
Dim ipos As Integer = posArticle(articles, articleNouveau.id)
' jeśli nie istnieje
If ipos = -1 Then Return 0
' istnieje – modyfikujemy go
articles(ipos) = articleNouveau
' zwracamy wynik
Return 1
End SyncLock
End Function
' usunięcie artykułu zidentyfikowanego na podstawie klucza
Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
' usunięcie artykułu
SyncLock Me
' sprawdzamy, czy istnieje
Dim ipos As Integer = posArticle(articles, idArticle)
' w przypadku, gdy nie istnieje
If ipos = -1 Then Return 0
' artykuł istnieje – usuwamy go
articles.RemoveAt(ipos)
' zwracamy wynik
Return 1
End SyncLock
End Function
' zmieniamy stan magazynowy artykułu zidentyfikowanego za pomocą klucza
Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
' zmiana stanu magazynowego artykułu
SyncLock Me
' sprawdzamy, czy istnieje
Dim ipos As Integer = posArticle(articles, idArticle)
' w przypadku, gdy nie istnieje
If ipos = -1 Then Return 0
' artykuł istnieje – modyfikuje się jego stan magazynowy, jeśli to możliwe
Dim unArticle As Article = CType(articles(ipos), Article)
' stan magazynowy zmieniamy tylko wtedy, gdy jest wystarczający
If unArticle.stockactuel + mouvement >= 0 Then
unArticle.stockactuel += mouvement
Return 1
Else
Return 0
End If
End SyncLock
End Function
' wyszukaj artykuł identyfikowany za pomocą klucza
Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
' zwraca pozycję artykułu [idArticle] na liście lub -1, jeśli nie został znaleziony
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
' nie znaleziono
Return -1
End Function
End Class
End Namespace
Uwagi:
- źródło danych jest symulowane przez pole prywatne [articles] typu [ArrayList]
- konstruktor klasy domyślnie tworzy 4 pozycje w źródle danych.
- wszystkie metody dostępu do danych zostały zsynchronizowane w celu uniknięcia problemów związanych z równoczesnym dostępem do źródła danych. W danym momencie tylko jeden wątek ma dostęp do danej metody.
- Metoda [posArticle] pozwala ustalić pozycję [0..N] w źródle [ArrayList] dla pozycji zidentyfikowanej na podstawie jej numeru. Jeśli artykuł nie istnieje, metoda zwraca pozycję -1. Metoda ta jest wielokrotnie wykorzystywana przez inne metody.
- Metoda [ajouteArticle] pozwala dodać artykuł do listy artykułów. Zwraca liczbę dodanych artykułów: 1. Jeśli artykuł już istniał, generowany jest wyjątek.
- Metoda [modifieArticle] umożliwia modyfikację istniejącego artykułu. Zwraca liczbę zmodyfikowanych artykułów: 1, jeśli artykuł istniał, w przeciwnym razie 0.
- Metoda [supprimeArticle] umożliwia usunięcie istniejącego artykułu. Zwraca liczbę usuniętych artykułów: 1, jeśli artykuł istniał, w przeciwnym razie 0.
- Metoda [getAllArticles] zwraca listę wszystkich artykułów
- Metoda [getArticleById] pozwala uzyskać artykuł identyfikowany na podstawie jego numeru. Jeśli artykuł nie istnieje, zwracana jest wartość [nothing].
- Kod nie stanowi większej trudności. Pozostawiamy czytelnikowi jego przejrzenie i zrozumienie.
1.4.3.4. Generowanie zestawu (assembly) warstwy [dao]
Projekt Visual Studio jest skonfigurowany do generowania zestawu [webarticles-dao.dll]. Zestaw ten jest generowany w folderze [bin] projektu:
![]() | ![]() |
1.4.3.5. Testy NUnit warstwy [dao]
W Javie klasy są testowane przy użyciu frameworka [Junit]. W .NET framework Nunit oferuje te same możliwości testów jednostkowych:

Struktura projektu testowego w Visual Studio wygląda następująco:

Uwagi:
- projekt [tests] jest typu [bibliothèque de classes]
- testy [NUnit] wymagają odwołania do zestawu [nunit.framework.dll]
- klasa testowa [NUnit] pobiera instancję testowanego obiektu za pośrednictwem Springa. Znajduje się więc
- w folderze [bin] pliki klas Springa
- w [References] – odwołanie do zestawu [Spring-Core.dll] z folderu [bin]
- w [bin] – plik konfiguracyjny dla Springa
- klasa testowa wymaga zestawu [webarticles-dao.dll] z warstwy [dao]. Został on umieszczony w folderze [bin], a jego odniesienie dodano do odniesień projektu.
Klasa testowa [NUnit] wymaga dostępu do klas z przestrzeni nazw [NUnit.Framework]. Znajduje się w niej zatem następująca instrukcja importu:
Przestrzeń nazw [NUnit.Framework] znajduje się w „assembly” [nunit.framework.dll], które należy dodać do odwołań projektu:
![]() | ![]() |
Zestaw [nunit.framework.dll] powinien znajdować się na wyświetlonej liście, jeśli zainstalowano już zestaw [Nunit]. Wystarczy dwukrotnie kliknąć na zestaw, aby dodać go do projektu:

Klasa testowa [NUnit] z warstwy [dao] mogłaby wyglądać następująco:
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
' obiekt do testowania
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' pobieramy instancję generatora obiektów Spring
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' żądamy instancjonowania obiektu articlesdao
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
End Sub
<Test()> _
Public Sub testGetAllArticles()
' weryfikacja wizualna
listArticles()
End Sub
<Test()> _
Public Sub testClearAllArticles()
' usuwamy wszystkie artykuły
articlesDao.clearAllArticles()
' pobieramy wszystkie artykuły
Dim articles As IList = articlesDao.getAllArticles
' sprawdzenie: powinno być ich 0
Assert.AreEqual(0, articles.Count)
End Sub
<Test()> _
Public Sub testAjouteArticle()
' usunięcie wszystkich pozycji
articlesDao.clearAllArticles()
' weryfikacja: tabela artykułów musi być pusta
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' dodajemy dwa artykuły
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' sprawdzenie: musi być dwa artykuły
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' kontrola wizualna
listArticles()
End Sub
<Test()> _
Public Sub testSupprimeArticle()
' usunięcie wszystkich artykułów
articlesDao.clearAllArticles()
' weryfikacja: tabela artykułów musi być pusta
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' dodajemy dwa artykuły
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' sprawdzenie: muszą być 2 pozycje
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' usuwamy pozycję nr 4
articlesDao.supprimeArticle(4)
' sprawdzenie: musi pozostać 1 artykuł
articles = articlesDao.getAllArticles
Assert.AreEqual(1, articles.Count)
' kontrola wzrokowa
listArticles()
End Sub
<Test()> _
Public Sub testModifieArticle()
' usunięcie wszystkich pozycji
articlesDao.clearAllArticles()
' weryfikacja
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' dodanie 2 artykułów
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' weryfikacja
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' wyszukiwanie pozycji 3
Dim unArticle As Article = articlesDao.getArticleById(3)
' weryfikacja
Assert.AreEqual(unArticle.nom, "article3")
' wyszukiwanie pozycji 4
unArticle = articlesDao.getArticleById(4)
' weryfikacja
Assert.AreEqual(unArticle.nom, "article4")
' modyfikacja artykułu 4
articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
' weryfikacja
unArticle = articlesDao.getArticleById(4)
Assert.AreEqual(unArticle.prix, 44, 0.000001)
' kontrola wzrokowa
listArticles()
End Sub
<Test()> _
Public Sub testGetArticleById()
' usunięcie artykułów
articlesDao.clearAllArticles()
' weryfikacja
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' dodanie 2 artykułów
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' weryfikacja
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' wyszukiwanie artykułu 3
Dim unArticle As Article = articlesDao.getArticleById(3)
' weryfikacja
Assert.AreEqual(unArticle.nom, "article3")
' wyszukiwanie artykułu 4
unArticle = articlesDao.getArticleById(4)
' weryfikacja
Assert.AreEqual(unArticle.nom, "article4")
End Sub
' wyświetlenie listy
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()
' usunięcie wszystkich artykułów
articlesDao.clearAllArticles()
' wyszukiwanie pozycji 1
Dim article As article = articlesDao.getArticleById(1)
' weryfikacja
Assert.IsNull(article)
' modyfikacja nieistniejącej pozycji
Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
' nie zmodyfikowano żadnego wiersza
Assert.AreEqual(i, 0)
' usunięcie nieistniejącego artykułu
i = articlesDao.supprimeArticle(1)
' nie usunięto żadnego wiersza
Assert.AreEqual(0, i)
End Sub
<Test()> _
Public Sub testChangerStockArticle()
' usunięcie wszystkich artykułów
articlesDao.clearAllArticles()
' dodanie artykułu
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' dodano artykuł
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' utworzenie 100 wątków
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' tworzymy wątek i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' ustalanie nazwy wątku
taches(i).Name = "tache_" & i
' uruchamia się wątek i
taches(i).Start()
Next
' oczekiwanie na zakończenie wszystkich wątków
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' sprawdzenie – stan magazynowy artykułu 3 musi wynosić 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' zmniejszamy stan magazynowy artykułu 4
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' sprawdzenie: jego stan magazynowy nie powinien ulec zmianie
Assert.AreEqual(0, nbLignes)
' kontrola wizualna
listArticles()
End Sub
Public Sub décrémente()
' uruchomiono wątek
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' wątek zmniejsza stan magazynowy
articlesDao.changerStockArticle(3, -1)
' wątek zakończony
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
End Class
End Namespace
Uwagi:
- chcieliśmy napisać program testujący interfejs [IArticlesDao], który byłby niezależny od klasy implementującej ten interfejs. W tym celu wykorzystaliśmy bibliotekę Spring, aby ukryć przed programem testującym nazwę klasy implementującej.
- Metoda atrybutu <Setup()> pobiera z biblioteki Spring odwołanie do obiektu [articlesdao], który ma zostać przetestowany. Obiekt ten jest zdefiniowany w następującym pliku [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>
Ten plik podaje nazwę klasy implementacyjnej [istia.st.articles.dao.ArticlesDaoArrayList] dla interfejsu [IArticlesDao] oraz informację, gdzie można ją znaleźć: [ webarticles-dao.dll]. Ponieważ instancjonowanie nie wymaga żadnych parametrów, nie zdefiniowano ich tutaj.
- Większość testów jest łatwa do zrozumienia. Zachęcamy czytelnika do zapoznania się z komentarzami.
- Metoda [testChangerStockArticle] wymaga kilku wyjaśnień. Tworzy ona 100 wątków odpowiedzialnych za zmniejszanie stanu magazynowego danego artykułu.
<Test()> _
Public Sub testChangerStockArticle()
' usunięcie wszystkich artykułów
articlesDao.clearAllArticles()
' dodanie pozycji
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' dodanie artykułu
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' utworzenie 100 wątków
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' tworzymy wątek i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' ustalanie nazwy wątku
taches(i).Name = "tache_" & i
' uruchamia się wątek i
taches(i).Start()
Next
' oczekiwanie na zakończenie wszystkich wątków
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' sprawdzenie – artykuł 3 musi mieć stan magazynowy równy 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' zmniejsza się stan magazynowy artykułu nr 4
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' weryfikacja: stan magazynowy tego artykułu nie powinien ulec zmianie
Assert.AreEqual(0, nbLignes)
' kontrola wzrokowa
listArticles()
End Sub
Celem tego testu jest sprawdzenie współbieżnego dostępu do źródła danych. Metoda odpowiedzialna za aktualizację stanu zapasów wygląda następująco:
Public Sub décrémente()
' uruchomiono wątek
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' wątek zmniejsza stan magazynowy
articlesDao.changerStockArticle(3, -1)
' wątek zakończony
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
Zmniejsza stan magazynowy artykułu nr 3 o jedną jednostkę. Jeśli spojrzymy na kod metody [testChangerStockArticle], widzimy, że:
- stan magazynowy artykułu nr 3 jest inicjowany na 101
- 100 wątków zmniejszy ten stan o jedną jednostkę każdy
- w związku z tym po zakończeniu działania wszystkich wątków stan zapasów powinien wynosić 1
Ponadto, nadal w tej metodzie, próbujemy ustawić stan magazynowy artykułu nr 4 na wartość ujemną. Próba ta musi zakończyć się niepowodzeniem.
Aby przetestować warstwę [dao], generujemy pliki DLL i [tests-webarticles-dao.dll] w folderze [bin] projektu [tests]:
![]() | ![]() |
Następnie, korzystając z aplikacji [Nunit-Gui], wczytujemy ten plik DLL i przeprowadzamy testy:

W lewym oknie widoczna jest lista testowanych metod. Kolor kropki przed nazwą każdej metody wskazuje, czy metoda zakończyła się powodzeniem (zielony) czy niepowodzeniem (czerwony). Czytelnik przeglądający ten dokument na ekranie będzie mógł zauważyć, że wszystkie testy zakończyły się powodzeniem. W dalszej części będziemy zakładać, że dysponujemy działającą warstwą [dao].
1.4.4. Warstwa [domain]
Warstwa [domain] zawiera następujące elementy:
-
[IArticlesDomain]: interfejs dostępu do warstwy [domain]
-
[Achat]: klasa definiująca zakup
-
[Panier]: klasa definiująca koszyk zakupów
-
[AchatsArticles]: klasa implementująca interfejs [IArticlesDomain]
Struktura rozwiązania [Visual Studio] warstwy [domain] jest następująca:

Uwagi:
- projekt [domain] jest typu [bibliothèque de classes]
- klasy zostały umieszczone w drzewie katalogów, którego katalogiem głównym jest folder [istia]. Wszystkie znajdują się w przestrzeni nazw [istia.st.articles.domain].
- Klasa DLL z warstwy [dao] została umieszczona w folderze [bin] nowego projektu. Ponadto klasa DLL została dodana do projektu jako odniesienie.
1.4.4.1. Interfejs [IArticlesDomain]
Interfejs [IArticlesDomain] oddziela warstwę [métier] od warstwy [web]. Ta ostatnia uzyskuje dostęp do warstwy [métier/domain] za pośrednictwem tego interfejsu, nie zwracając uwagi na klasę, która faktycznie go implementuje. Interfejs definiuje następujące akcje dotyczące dostępu do warstwy biznesowej:
Imports Article = istia.st.articles.dao.Article
Namespace istia.st.articles.domain
Public Interface IArticlesDomain
' metody
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
zwraca listę obiektów [Article] z powiązanego źródła danych | |
zwraca obiekt [Article] identyfikowany przez [idArticle] | |
zatwierdza koszyk klienta, zmniejszając stany magazynowe zakupionych artykułów o zakupioną ilość – może zakończyć się niepowodzeniem, jeśli stan magazynowy jest niewystarczający | |
zwraca listę wystąpionych błędów – pusta, jeśli nie ma błędów |
1.4.4.2. Klasa [Achat]
Klasa [Achat] reprezentuje zakup dokonywany przez klienta:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class Achat
' pola prywatne
Private _article As article
Private _qte As Integer
' konstruktor domyślny
Public Sub New()
End Sub
' konstruktor z parametrami
Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
' przechodzimy przez właściwości
Me.article = unArticle
Me.qte = qte
End Sub
' zakupiony artykuł
Public Property article() As article
Get
Return _article
End Get
Set(ByVal Value As article)
_article = Value
End Set
End Property
' ilość zakupiona
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
' suma zakupu
Public ReadOnly Property totalAchat() As Double
Get
Return _qte * _article.prix
End Get
End Property
' identyfikator
Public Overrides Function ToString() As String
Return "[" + _article.ToString + "," + _qte.ToString + "]"
End Function
End Class
End Namespace
Uwagi:
- klasa [Achat] posiada następujące właściwości i metody:
zakupiony artykuł | |
zakupiona ilość | |
kwota zakupu | |
ciąg identyfikacyjny obiektu |
- posiada konstruktor umożliwiający zainicjowanie właściwości [article, qte], które definiują zakup.
1.4.4.3. Klasa [Panier]
Klasa [Panier] reprezentuje zbiór zakupów klienta:
Namespace istia.st.articles.domain
Public Class Panier
' pola prywatne
Private _achats As New ArrayList
Private _totalPanier As Double = 0
' domyślny producent
Public Sub New()
End Sub
' lista zakupów
Public ReadOnly Property achats() As ArrayList
Get
Return _achats
End Get
End Property
' suma zakupów
Public ReadOnly Property totalPanier() As Double
Get
Return _totalPanier
End Get
End Property
' metody
Public Sub ajouter(ByVal unAchat As Achat)
' sprawdzamy, czy zakup już istnieje
Dim iAchat As Integer = posAchat(unAchat.article.id)
If iAchat <> -1 Then
' znaleziono
Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
achatCourant.qte += unAchat.qte
Else
' nie znaleziono
_achats.Add(unAchat)
End If
' zwiększamy sumę w koszyku
_totalPanier += unAchat.totalAchat
End Sub
' usuwanie pozycji z koszyka
Public Sub enlever(ByVal idAchat As Integer)
' szukamy produktu
Dim iachat As Integer = posAchat(idAchat)
' jeśli znaleziono, usuwamy
If iachat <> -1 Then
Dim achatCourant As Achat = CType(_achats(iachat), Achat)
' usuwamy z koszyka
_achats.RemoveAt(iachat)
' zmniejszamy sumę koszyka
_totalPanier -= achatCourant.totalAchat
End If
End Sub
Private Function posAchat(ByVal idArticle As Integer) As Integer
' wyszukuje zakup na liście zakupów
' zwraca pozycję na liście lub -1, jeśli nie znaleziono
Dim achatCourant As Achat
Dim trouvé As Boolean = False
Dim i As Integer = 0
While Not trouvé AndAlso i < _achats.Count
' bieżący zakup
achatCourant = CType(_achats(i), Achat)
' porównanie z poszukiwanym produktem
If achatCourant.article.id = idArticle Then
Return i
End If
'następny zakup
i += 1
End While
' nie znaleziono
Return -1
End Function
' funkcja tożsamości
Public Overrides Function ToString() As String
Return _achats.ToString
End Function
End Class
End Namespace
Uwagi:
- klasa [Panier] posiada następujące właściwości i metody:
lista zakupów klienta – lista obiektów typu [Achat] | |
dodaje zakup do listy zakupów | |
usuwa zakup artykułu idAchat | |
łączna kwota zakupów w koszyku | |
zwraca ciąg identyfikacyjny koszyka |
- metoda [posAchat] jest metodą pomocniczą, która pozwala uzyskać pozycję na liście zakupów dla zakupu zidentyfikowanego na podstawie numeru zakupionego artykułu. Lista zakupów jest zarządzana w taki sposób, że artykuł zakupiony wielokrotnie zajmuje tylko jedną pozycję na liście. Dzięki temu zakup można zidentyfikować na podstawie numeru zakupionego artykułu. Metoda [posAchat] zwraca -1, jeśli szukany zakup nie istnieje.
- Metoda [ajouter] dodaje nowy zakup do listy zakupów. Oznacza to albo dodanie nowego wpisu do listy zakupów, jeśli zakupiony artykuł nie występował jeszcze na liście, albo zwiększenie ilości zakupionego towaru, jeśli już tam występował.
- Metoda [enlever] pozwala usunąć z listy zakupów pozycję oznaczoną numerem. Jeśli pozycja nie istnieje, metoda nie generuje żadnego komunikatu i nie wykonuje żadnej czynności.
- Kwota zakupów [totalPanier] jest aktualizowana na bieżąco w miarę dodawania i usuwania zakupów.
1.4.4.4. Klasa [AchatsArticles]
Interfejs [IArticlesDomain] zostanie zaimplementowany przez następującą klasę [AchatsArticles]:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class AchatsArticles
Implements IArticlesDomain
'pola prywatne
Private _articlesDao As IArticlesDao
Private _erreurs As ArrayList
' konstruktor
Public Sub New(ByVal articlesDao As IArticlesDao)
_articlesDao = articlesDao
End Sub
' lista błędów
Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
Get
Return _erreurs
End Get
End Property
' lista artykułów
Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
' lista wszystkich artykułów
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
' pobierz artykuł o określonym numerze
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
' konkretny artykuł
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
' zakup koszyka
Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
' zakup koszyka – stany magazynowe zakupionych artykułów muszą zostać pomniejszone
_erreurs = New ArrayList
Dim achat As achat
Dim achats As ArrayList = panier.achats
For i As Integer = achats.Count - 1 To 0 Step -1
' zmniejszyć stan magazynowy artykułu i
achat = CType(achats(i), achat)
Try
If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
' nie udało się wykonać operacji
_erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
Else
' operacja została wykonana – usunięto zakup z koszyka
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
Uwagi:
- ta klasa implementuje cztery metody interfejsu [IArticlesDomain]. Posiada dwa pola prywatne:
obiekt dostępu do danych | |
lista ewentualnych błędów. Dostęp do niej zapewnia publiczna właściwość [erreurs] |
- Aby utworzyć instancję tej klasy, należy podać obiekt umożliwiający dostęp do danych:
- Metody [getAllArticles] i [getArticleById] opierają się na metodach o tej samej nazwie z warstwy [dao]
- Metoda [acheter] zatwierdza zakup zawartości koszyka. Zatwierdzenie to polega po prostu na zmniejszeniu stanów magazynowych zakupionych artykułów. Zakup artykułu jest możliwy tylko wtedy, gdy pozwala na to stan magazynowy. Jeśli tak nie jest, zakup zostaje odrzucony: artykuł pozostaje w koszyku, a na liście [erreurs] zgłaszany jest błąd. Zatwierdzony zakup jest usuwany z koszyka, a stan magazynowy danego artykułu zmniejszany o zakupioną ilość.
1.4.4.5. Generowanie zestawu warstwy [domain]
Projekt Visual Studio jest skonfigurowany do generowania zestawu [webarticles-domain.dll]. Zestaw ten jest generowany w folderze [bin] projektu:
![]() | ![]() |
1.4.4.6. Testy NUnit warstwy [domain]
Struktura projektu testowego w Visual Studio wygląda następująco:

Uwagi:
- projekt [tests] jest typu [bibliothèque de classes]
- testy [NUnit] wymagają odwołania do zestawu [nunit.framework.dll]
- klasa testowa [NUnit] pobiera instancję testowanego obiektu za pośrednictwem Springa. Znajduje się więc
- w folderze [bin] pliki klas Springa
- w [References] – odwołanie do zestawu [Spring-Core.dll] z folderu [bin]
- w [bin] – plik konfiguracyjny dla Springa
- klasa testowa wymaga zestawu [webarticles-dao.dll] z warstwy [dao] oraz zestawu [webarticles-domain.dll] z warstwy [domain]. Zostały one umieszczone w folderze [bin], a ich odwołania dodano do odwołań projektu.
Klasa testowa NUnit z warstwy [domain] mogłaby wyglądać następująco:
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
' obiekt do testowania
Private articlesDomain As IArticlesDomain
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' pobieramy instancję generatora obiektów Spring
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' żądamy instancji obiektu DAO typu „articles”
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
' a następnie obiektu „articlesdomain”
articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
End Sub
<Test()> _
Public Sub getAllArticles()
' wizualna weryfikacja
listArticles()
End Sub
<Test()> _
Public Sub getArticleById()
' usuwanie artykułów
articlesDao.clearAllArticles()
' weryfikacja
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' dodanie 2 artykułów
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' weryfikacja
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' wyszukiwanie artykułu 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' weryfikacja
Assert.AreEqual(unArticle.nom, "article3")
' wyszukiwanie artykułu 4
unArticle = articlesDao.getArticleById(4)
' weryfikacja
Assert.AreEqual(unArticle.nom, "article4")
End Sub
<Test()> _
Public Sub acheterPanier()
' usunięcie artykułów
articlesDao.clearAllArticles()
' weryfikacja
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' dodanie 2 artykułów
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' weryfikacja
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' utworzenie koszyka z dwoma produktami
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))
' weryfikacje
Assert.AreEqual(700, panier.totalPanier, 0.000001)
Assert.AreEqual(2, panier.achats.Count)
' zatwierdzenie koszyka
articlesDomain.acheter(panier)
' weryfikacje
Assert.AreEqual(0, articlesDomain.erreurs.Count)
Assert.AreEqual(0, panier.achats.Count)
' wyszukiwanie produktu 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' weryfikacja
Assert.AreEqual(unArticle.stockactuel, 20)
' wyszukiwanie produktu 4
unArticle = articlesDao.getArticleById(4)
' weryfikacja
Assert.AreEqual(unArticle.stockactuel, 30)
' nowy koszyk
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
' potwierdzenie koszyka
articlesDomain.acheter(panier)
' weryfikacje
Assert.AreEqual(1, articlesDomain.erreurs.Count)
' wyszukiwanie produktu 3
unArticle = articlesDomain.getArticleById(3)
' weryfikacja
Assert.AreEqual(unArticle.stockactuel, 20)
End Sub
<Test()> _
Public Sub testRetirerAchats()
' usunięcie zawartości z ARTICLES
articlesDao.clearAllArticles()
' odczytuje tabelę ARTICLES
Dim articles As IList = articlesDao.getAllArticles()
Assert.AreEqual(0, articles.Count)
' wstawianie
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)
' odczytuje tabelę ARTICLES
articles = articlesDomain.getAllArticles()
Assert.AreEqual(2, articles.Count)
' utworzenie koszyka z dwoma produktami
Dim monPanier As New Panier
monPanier.ajouter(New Achat(article3, 10))
monPanier.ajouter(New Achat(article4, 10))
' weryfikacje
Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
Assert.AreEqual(2, monPanier.achats.Count)
' dodanie już zakupionego produktu
monPanier.ajouter(New Achat(article3, 10))
' weryfikacje
' suma musi zostać zmieniona na 1000
Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
' w koszyku nadal znajdują się 2 produkty
Assert.AreEqual(2, monPanier.achats.Count)
' ilość produktu 3 musi zostać zmieniona na 20
Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
Assert.AreEqual(20, unAchat.qte)
' usuwamy produkt nr 3 z koszyka
monPanier.enlever(3)
' sprawdzenie
' suma musi wynosić 400
Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
' W koszyku znajduje się tylko 1 pozycja
Assert.AreEqual(1, monPanier.achats.Count)
' musi to być produkt nr 4
Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
End Sub
' wyświetlanie na ekranie
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
Komentarze:
- chcieliśmy napisać program testujący interfejs [IArticlesDomain], który byłby niezależny od klasy implementującej ten interfejs. W tym celu wykorzystaliśmy bibliotekę Spring, aby ukryć przed programem testującym nazwę klasy implementującej.
- Metoda atrybutu <Setup()> pobiera ze Springa odwołania do obiektów [articlesdomain] i [articlesdao], które mają zostać przetestowane. Obiekty te są zdefiniowane w poniższym pliku [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>
Ten plik zawiera:
- (ciąg dalszy)
- dla singletonu [articlesdao] – nazwę klasy implementacyjnej [istia.st.articles.dao.ArticlesDaoArrayList] oraz lokalizację, w której można ją znaleźć: [ webarticles-dao.dll]. Ponieważ instancjonowanie nie wymaga parametrów, nie zdefiniowano tu żadnych.
- dla singletonu [articlesdomain] podano nazwę klasy implementacyjnej [istia.st.articles.domain.AchatsArticles] oraz lokalizację klasy [ webarticles-domain.dll]. Klasa [AchatsArticles] posiada konstruktor z jednym parametrem: singletonem zarządzającym dostępem do warstwy [dao]. W tym przypadku jest to singleton [articlesdao] zdefiniowany wcześniej.
- Klasa testowa uzyskuje instancję klasy testowanej [articlesdomain], a także instancję klasy dostępu do danych [articlesdao]. Ta ostatnia kwestia budzi wątpliwości. Teoretycznie klasa testowa nie powinna potrzebować dostępu do warstwy [dao], o której nawet nie powinna wiedzieć. W tym przypadku zignorowaliśmy tę „etykę”, której przestrzeganie zmusiłoby nas do utworzenia nowych metod w naszym interfejsie [IArticlesDomain].
Aby przetestować warstwę [domain], generujemy pliki DLL i [tests-webarticles-domain.dll] w folderze [bin] projektu [tests]:
![]() | ![]() |
Następnie, korzystając z aplikacji [Nunit-Gui], wczytujemy ten plik DLL i przeprowadzamy testy:

Użytkownik przeglądający ten dokument na ekranie zobaczy, że wszystkie testy zakończyły się powodzeniem. Następnie uznamy, że mamy gotową do użycia warstwę [domain].
1.4.5. Wniosek
Przypomnijmy, że chcemy zbudować następującą trójwarstwową aplikację internetową:
![]() |
Model M naszej aplikacji MVC został już napisany i przetestowany. Jest on dostarczony w dwóch plikach: DLL i [webarticles-dao.dll, webarticles-domain.dll]. Możemy przejść do ostatniej warstwy, warstwy [web], która zawiera kontroler C oraz widoki V. Najpierw przyjrzymy się metodzie przedstawionej w dokumencie [Développement WEB avec ASP.NET 1.1 ]
- kontroler C jest realizowany przez dwa pliki [global.asax, main.aspx]
- widoki V są realizowane przez strony aspx
1.5. Warstwa [web]
Architektura aplikacji internetowej MVC będzie wyglądać następująco:
![]() |
klasy biznesowe [domain], klasy dostępu do danych [dao] oraz źródło danych | |
strony ASPX | |
wszystkie żądania klientów HTTP przechodzą przez następujące dwa kontrolery: global.asax: obsługuje zdarzenia związane z pierwszym uruchomieniem aplikacji main.aspx: indywidualnie przetwarza żądania poszczególnych klientów |
1.5.1. Widoki
Widoki są takie same jak te przedstawione na początku dokumentu:
liste.aspx | Widoki są zgromadzone w folderze [vues] aplikacji ![]() | |
infos.aspx | ||
panier.aspx | ||
paniervide.aspx | ||
erreurs.aspx |
1.5.2. Kontrolery
Jak już wspomniano, kontroler będzie składał się z dwóch elementów:
- [global.asax,global.asax.vb]: służy głównie do zainicjowania aplikacji i umieszczenia w jej kontekście wszystkich danych, które mają być współdzielone między różnymi klientami
- [main.aspx, main.aspx.vb]: właściwy kontroler, który przetwarza żądania HTTP od klientów.
Różne żądania klientów będą kierowane do kontrolera [main.aspx] i będą zawierały parametr o nazwie [action], określający akcję żądaną przez klienta:
znaczenie | znaczenie | działanie kontrolera | możliwe odpowiedzi |
klient chce otrzymać listę artykułów | – prosi o listę artykułów na poziomie biznesowej | – [LISTE] - [ERREURS] | |
klient prosi o informacji na temat jednego z artykułów wyświetlanych w widoku [LISTE] | – wysyła zapytanie o artykuł do warstwy biznesowej | - [INFOS] - [ERREURS] | |
klient kupuje artykuł | - wysyła zapytanie o produkt do warstwy biznesowej i umieszcza go w koszyku klienta | - [INFOS] w przypadku błędu ilości - [LISTE], jeśli nie ma błędu | |
klient chce usunąć zakup z koszyka | - pobiera koszyk z sesji i modyfikuje go | - [PANIER] - [PANIERVIDE] - [ERREURS] | |
klient chce wyświetlić swój koszyk | - pobiera koszyk z sesji | - [PANIER] - [PANIERVIDE] - [ERREURS] | |
klient zakończył zakupy i przechodzi do etapu płatności | - aktualizuje stan magazynowy zakupionych produktów - usuwa z koszyka klienta produkty, , których zakup został zatwierdzony | - [LISTE] - [ERREURS] |
1.5.3. Konfiguracja aplikacji
Postaramy się skonfigurować aplikację tak, aby była jak najbardziej elastyczna w obliczu zmian, takich jak:
- zmiana klas URL w poszczególnych widokach
- zmiana klas implementujących interfejsy [IArticlesDao] i [IArticlesDomain]
- zmiana SGBD, bazy danych oraz tabeli artykułów
1.5.3.1. Zmiany w URL
Nazwy widoków URL zostaną umieszczone w pliku konfiguracyjnym aplikacji [web.config] wraz z kilkoma innymi parametrami:
<?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. Zmiana klas implementujących interfejsy
Zgodnie z zasadami architektury trójwarstwowej warstwy muszą być od siebie odizolowane. Izolację tę osiąga się w następujący sposób:
- warstwy komunikują się ze sobą za pośrednictwem interfejsów, a nie konkretnych klas
- kod danej warstwy nigdy sam nie instancjonuje klasy z innej warstwy w celu jej wykorzystania. Po prostu zwraca się do narzędzia zewnętrznego, w tym przypadku Springa, o instancję implementacji interfejsu warstwy, z której chce skorzystać. W tym celu wiemy, że nie musi znać nazwy klasy implementującej, a jedynie nazwę singletonu Springa, do którego chce uzyskać odwołanie.
W naszej aplikacji Spring zostanie skonfigurowany w pliku [web.config] aplikacji internetowej w następujący sposób:
<?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>
Aby uzyskać dostęp do warstwy [métier], klasa z warstwy [web] może zażądać singletonu [articlesDomain]. Spring utworzy wówczas instancję obiektu typu [istia.st.articles.domain.AchatsArticles]. Do tej instancji potrzebny jest obiekt typu [articlesDao], czyli obiekt typu [istia.st.articles.dao.ArticlesDaoArrayList]. Spring utworzy wówczas taki obiekt. Po zakończeniu operacji warstwa [web], która zażądała singletonu [articlesDomain], posiada cały łańcuch łączący ją ze źródłem danych:
![]() |
1.5.3.3. Zmiany związane z SGBD lub bazą danych
Ten punkt zostanie tutaj pominięty, ponieważ mamy do czynienia z aplikacją testową bez warstwy SGBD. W kolejnym etapie omówimy implementację warstwy [dao] opartej na warstwie SGBD.
1.5.4. Biblioteka tagów <asp:>
Rozważmy widok [ERREURS], który wyświetla listę błędów:

Widok [ERREURS] ma za zadanie wyświetlić listę błędów, którą kontroler [main.aspx] umieścił w kontekście żądania pod nazwą [context.Items("erreurs")]. Istnieje kilka sposobów napisania takiej strony. W tym miejscu skupiamy się wyłącznie na części odpowiedzialnej za wyświetlanie błędów.
Przypomnijmy, że strona ASPX składa się z części prezentacyjnej HTML oraz części kodowej .NET, która przygotowuje dane, które ma wyświetlić część prezentacyjna. Te dwie części mogą znajdować się w tym samym pliku [aspx] (rozwiązanie WebMatrix) lub w dwóch plikach: [aspx] dla części prezentacyjnej, [aspx.vb] dla kodu. To ostatnie rozwiązanie jest stosowane w programie Visual Studio. Aby jeszcze bardziej skomplikować sprawę, część prezentacyjna HTML może również zawierać kod .NET, co zniekształca widoczny podział na [contrôleur] i [présentation]. Zazwyczaj zdecydowanie odradza się stosowanie tego rozwiązania. Usunięcie całego kodu z części [présentation] wymagało utworzenia bibliotek znaczników. Biblioteki te „ukrywają” kod pod postacią znaczników analogicznych do znaczników HTML. Przedstawiamy dwa możliwe rozwiązania dla strony [ERREURS].
Nasze pierwsze rozwiązanie wykorzystuje kod .NET w części [présentation] strony. Strona ASPX pobiera listę błędów zawartych w żądaniu w swojej części kontrolera [erreurs.aspx.vb]:
Protected erreurs As ArrayList
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
' pobieramy błędy
erreurs = CType(context.Items("erreurs"), ArrayList)
End Sub
a następnie wyświetla to w sekcji [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>
Drugie rozwiązanie wykorzystuje tag <asp:repeater> z biblioteki tagów <asp:> w ASP.NET. Jeśli tworzymy stronę ASPX graficznie, tag ten jest dostępny w postaci komponentu serwerowego, który umieszczamy na formularzu projektowym. Jeśli kod ASPX tworzymy ręcznie, możemy mówić o bibliotece tagów.
Dzięki bibliotece tagów <asp:> kod ASPX z poprzedniego widoku [ERREURS] przyjmuje następującą postać:
<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>
Tag
służy do powtórzenia wzorca HTML w różnych elementach źródła danych. Jego poszczególne elementy są następujące:
motyw HTML, który ma być wyświetlany przed wyświetleniem elementów źródła danych | |
motyw HTML, który ma być powtarzany dla każdego elementu źródła danych. Wyrażenie [<%# Container.DataItem %>] służy do wyświetlenia wartości bieżącego elementu źródła danych | |
wzorzec HTML, który ma zostać wyświetlony po wyświetleniu elementów źródła danych |
Źródło danych jest zazwyczaj powiązane z tagiem w części strony o nazwie [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
..
' powiązanie błędów z rptErreurs
With rptErreurs
.DataSource = context.Items("erreurs")
.DataBind()
End With
End Sub
Połączenie to można również nawiązać na etapie projektowania strony, jeśli źródło danych jest już znane, na przykład istniejąca baza danych.
W naszych widokach użyjemy innego tagu: <asp:datagrid>, który umożliwia wyświetlenie źródła danych w formie tabeli.
1.5.5. Struktura rozwiązania Visual Studio dla aplikacji [webarticles]
Aplikacja internetowa to układanka składająca się z wielu elementów. Nadanie jej architektury MVC zazwyczaj zwiększa ich liczbę. Struktura aplikacji [webarticles] w ramach [Visual Studio] wygląda następująco:
![]() | ![]() | ![]() |
![]() |
Uwagi:
- projekt [web] jest typu [bibliothèque de classes], a nie typu [Application web ASP.NET], jak można by logicznie oczekiwać. Typ [Application web ASP.NET] wymaga obecności serwera WWW IIS na komputerze deweloperskim lub na komputerze zdalnym. Serwer IIS nie występuje standardowo na komputerach z systemem Windows XP Familial. Jednak wiele komputerów z systemem PC jest sprzedawanych właśnie z tą wersją. Aby umożliwić czytelnikom korzystającym z systemu Windows XP wdrożenie omawianej aplikacji, wykorzystamy serwer WWW Cassini (patrz załącznik), dostępny bezpłatnie w firmie Microsoft, i zastąpimy projekt [Application web ASP.NET] projektem [bibliothèque de classes]. Wiąże się to z pewnymi niedogodnościami, które wyjaśniono w załącznikach.
- Pliki DLL wykorzystywane przez aplikację to:
zawiera klasy warstwy dostępu do danych | |
obejmuje klasy warstwy biznesowej | |
zawiera klasy Spring, które umożliwiają nam integrację warstw web, domain i DAO | |
klasy logów – wykorzystywane przez Spring |
Te pliki DLL umieszcza się w folderze [bin] i dodaje do referencji projektu.
1.5.6. Widoki ASPX
Zgodnie z wcześniejszymi zaleceniami w naszych widokach ASPX będziemy korzystać z biblioteki tagów <asp:>.
1.5.6.1. Komponent użytkownika [entete.ascx]
Aby zapewnić spójność poszczególnych widoków, będą one miały wspólny nagłówek, w którym wyświetlana jest nazwa aplikacji wraz z menu:
![]() | ![]() |
Menu jest dynamiczne i ustalane przez kontroler. Kontroler umieszcza w żądaniu przesyłanym do strony ASPX atrybut klucza „actions”, którego wartością jest tablica elementów typu Hashtable(). Każdy element tej tablicy jest słownikiem służącym do generowania opcji w menu nagłówka. Każdy słownik posiada dwa klucze:
- href: adres URL powiązany z opcją menu
- link: tekst menu
Nagłówek zostanie przekształcony w kontrolkę użytkownika. Kontrolka użytkownika hermetyzuje fragment strony (układ i powiązany kod) w komponent, który można następnie ponownie wykorzystać na innych stronach. W tym przypadku chcemy ponownie wykorzystać komponent [entete] w innych widokach aplikacji. Kod prezentacji będzie znajdował się w komponencie [entete.ascx], a powiązany kod sterujący w komponencie [entete.ascx.vb]. Kod prezentacji będzie wykorzystywał komponent <asp:repeater> do wyświetlenia tabeli opcji menu:
![]() |
n° | type | nom | rôle |
1 | repeater | rptMenu źródło danych: tablica słowników z dwoma kluczami: href, link | wyświetl opcje menu |
Kod wyświetlający stronę będzie wyglądał następująco:
Komentarze:
- Komponent [repeater] jest zdefiniowany w wierszach 6–14
- każdy element źródła danych powiązanego z powtarzaczem jest słownikiem zawierającym dwa klucze: href – wiersz 9 oraz link – wiersz 10
Odpowiedni kod kontrolny będzie wyglądał następująco:
Uwagi:
- komponent typu [EnteteWebArticles] posiada publiczną właściwość [actions] z uprawnieniami tylko do zapisu – wiersz 7
- właściwość ta pozwala powiązać z komponentem <asp:repeater> o nazwie [rptMenu] – wiersz 10 – tablicę opcji obliczoną przez kontroler aplikacji – wiersze 11–12.
Pozostałe widoki aplikacji będą korzystać z nagłówka zdefiniowanego przez [entete.ascx]. Na przykład strona [erreurs.aspx] będzie zawierać ten nagłówek za pomocą następującego kodu:
Komentarze:
- Wiersz 1 określa, że tag <WA:entete> powinien być powiązany z komponentem zdefiniowanym w pliku [entete.ascx]. Atrybuty [TagPrefix] i [TagName] mogą być dowolne.
- Po wykonaniu tej czynności wstawienie komponentu do kodu prezentacyjnego strony odbywa się za pomocą wiersza 9. Podczas wykonywania ten tag spowoduje włączenie do kodu strony ASPX, która go zawiera, kodu strony [entete.ascx]. Kod sterujący [erreurs.aspx.vb] zajmie się zainicjowaniem tego komponentu. Może to zrobić w następujący sposób:
Uwagi:
- w wierszu 6 tworzony jest obiekt typu [EnteteWebArticles], który jest typem tworzonego komponentu
- w wierszu 11 inicjowana jest właściwość [actions] tego obiektu
1.5.6.2. Widok [liste.aspx]
1.5.6.2.1. Introduction
Ten widok wyświetla listę artykułów dostępnych w sprzedaży:
![]() | ![]() |
Jest ona wyświetlana po wysłaniu żądania /main?action=liste lub /main?action=validationpanier. Elementy żądania kontrolera są następujące:
obiekt Hashtable() – tablica opcji menu | |
ArrayList obiektów typu [Article] | |
obiekt String – komunikat do wyświetlenia na dole strony |
Każdy link [Infos] z tabeli HTML artykułów ma URL w postaci [?action=infos&id=ID], gdzie ID jest polem idwyświetlanego artykułu.
1.5.6.2.2. Elementy strony
![]() |
n° | type | nom | rôle |
element użytkownika | nagłówek | wyświetl nagłówek | |
DataGrid | DataGridArticles 3 – kolumna powiązana: nagłówek: Nazwa, pole: nazwa 4 – kolumna powiązana: nagłówek: Cena, pole: cena 5 – kolumna hipertekstowa: tekst: Informacje, pole URL: id, format URL: /webarticles/main.aspx?action=infos&id={0} | wyświetl artykuły na sprzedaż | |
etykieta | lblMessage | wyświetl wiadomość |
Przypomnijmy, jak należy postępować, aby zdefiniować te właściwości:
- w programie Visual Studio należy zaznaczyć element [DataGrid], aby uzyskać dostęp do jego arkusza właściwości:

- używamy powyższego linku [Mise en forme automatique], aby zarządzać wyglądem wyświetlanej tabeli
- oraz link [Générateur de propriétés], aby zarządzać jego zawartością
1.5.6.2.3. Kod prezentacji [liste.aspx]
Uwagi:
- wiersz 9 definiuje nagłówek strony
- wiersze 12–24 definiują cechy [DataGrid]
- wiersz 26 definiuje etykietę [lblMessage]
1.5.6.2.4. Kod kontrolera [liste.aspx.vb]
Uwagi:
- elementy strony pojawiają się w wierszach 13–15. Należy zauważyć, że konieczne było utworzenie obiektu [EnteteWebArticles] z operatorem [new], podczas gdy w przypadku pozostałych elementów nie było to konieczne. Bez tego jawnego utworzenia pojawiał się błąd wykonania wskazujący, że obiekt [entete] nie odnosił się do niczego. Kwestia ta zasługiwałaby na dokładniejsze zbadanie. Nie została jednak zbadana.
- tabela opcji menu nagłówka jest pobierana z kontekstu w celu zainicjowania komponentu [entete] strony – wiersz 20
- lista artykułów jest pobierana z kontekstu – wiersz 22
- w celu zainicjowania komponentu [DataGridArticles] – wiersze 24–27
- komponent [lblMessage] jest inicjowany za pomocą komunikatu umieszczonego w kontekście – wiersz 29
1.5.6.3. Widok [infos.aspx]
1.5.6.3.1. Introduction
Ten widok wyświetla informacje o produkcie i umożliwia jego zakup:

Jest wyświetlany po wysłaniu żądania /main?action=infos&id=ID lub żądania /main?action=achat&id=ID, gdy ilość zakupiona jest nieprawidłowa. Elementy żądania kontrolera są następujące:
obiekt Hashtable() – tablica opcji menu | |
obiekt typu [Article] – artykuł do wyświetlenia | |
obiekt typu String – komunikat do wyświetlenia w przypadku błędu dotyczącego ilości | |
obiekt typu String – wartość do wyświetlenia w polu wprowadzania danych [Qte] |
Pola [msg] i [qte] są używane w przypadku błędu wprowadzenia ilości:

Ta strona zawiera formularz, który jest wysyłany za pomocą przycisku [Acheter]. Docelowym polem URL dla pola POST jest [?action=achat&id=ID], gdzie ID to identyfikator zakupionego artykułu.
1.5.6.3.2. Elementy strony
![]() |
n° | type | nom | rôle |
element użytkownika | nagłówek | wyświetl nagłówek | |
literał | litID | wyświetl numer artykułu | |
DataGrid | DataGridArticle 3 – kolumna powiązana: nagłówek: Nazwa, pole: nazwa 4 – kolumna powiązana: nagłówek: Cena, pole: cena 5 – kolumna powiązana: nagłówek: Aktualny stan magazynowy, pole: stockActuel 6 – kolumna powiązana: nagłówek: Minimalny stan magazynowy, pole: stockMinimum | wyświetl pozycję | |
HTML Prześlij | wyślij formularz | ||
HTML Input runat=server | txtQte | wprowadź ilość zakupioną | |
etykieta | lblMsgQte | ewentualny komunikat o błędzie |
1.5.6.3.3. Kod prezentacji [infos.aspx]
Uwagi:
- nagłówek jest zawarty na stronie – wiersz 9
- literał [litId] jest zdefiniowany w wierszu 10
- DataGrid i [DataGridArticles] są zdefiniowane w wierszach 12–34
- formularz jest zdefiniowany w wierszach 36–46. Ma on typ POST.
- Cel dla POST jest podawany przez zmienną [strAction] – wiersz 36. Zmienna ta musi zostać zdefiniowana przez kontrolera.
- Pole wprowadzania zakupionej ilości jest zdefiniowane w wierszu 41. Jest to komponent serwerowy HTML (runat=server). W kodzie dostęp do niego uzyskuje się za pośrednictwem obiektu.
- W wierszu 42 zdefiniowano etykietę [lblMsgQte], która będzie zawierać ewentualny komunikat o błędzie dotyczący wprowadzonej ilości
1.5.6.3.4. Kod kontrolny [infos.aspx.vb]
Komentarze:
- elementy strony są zdefiniowane w wierszach 10–14
- klasa definiuje publiczną właściwość [strAction], która służy do określenia miejsca docelowego pola POST w formularzu – wiersze 17–25
- wyświetlany artykuł jest pobierany z kontekstu aplikacji – wiersz 30
- tabela opcji menu nagłówkowego jest pobierana z kontekstu w celu zainicjowania komponentu [entete] strony – wiersz 32
- w wierszach 33–39 komponent [DataGridArticle] jest powiązany ze źródłem danych typu [ArrayList], zawierającym wyłącznie artykuł pobrany w wierszu 30
- komponenty [lblMsgQte, txtQte] są inicjowane na podstawie informacji pobranych z kontekstu – wiersze 42–45
- właściwość [straction] jest również inicjowana na podstawie informacji pobranych z kontekstu – wiersz 47. Zmienna ta służy do wygenerowania atrybutu [action] formularza HTML znajdującego się na stronie:
1.5.6.4. Widok [panier.aspx]
1.5.6.4.1. Introduction
Ten widok wyświetla zawartość koszyka:

Jest on wyświetlany po wysłaniu żądania /main?action=koszyk lub /main?action=usunąćzakup&id=ID. Elementy żądania kontrolera są następujące:
Obiekt Hashtable() – tabela opcji menu | |
obiekt typu [Panier] – koszyk do wyświetlenia |
Każdy link [Retirer] z tabeli HTML zawierającej zakupy w koszyku posiada URL o postaci [?action=retirerachat&id=ID], gdzie ID to pole [id] artykułu, który chcemy usunąć z koszyka.
1.5.6.4.2. Elementy strony
![]() |
n° | type | nom | rôle |
element użytkownika | nagłówek | wyświetl nagłówek | |
DataGrid | DataGridAchats 3 – kolumna powiązana – nagłówek: Artykuł, pole: nazwa 4 – kolumna powiązana – nagłówek: Ilość, pole: ilość 5 – kolumna powiązana – nagłówek: Cena, pole: cena 6 – kolumna powiązana – nagłówek: Suma, pole: suma, formatowanie {0:C} 7 – kolumna hipertekstowa – Tekst: Usuń, URL: id, format URL: /webarticles/main.aspx?action=retirerachat&id={0} | wyświetl listę zakupionych artykułów | |
etykieta | lblTotal | wyświetl kwotę do zapłaty |
1.5.6.4.3. Kod prezentacji [panier.aspx]
Uwagi
- wiersz 9 zawiera nagłówek
- w wierszach 12–27 zdefiniowano komponent [DataGridAchats]
- w wierszu 29 zdefiniowano komponent [lblTotal]
1.5.6.4.4. Kod kontrolny [panier.aspx.vb]
Uwagi:
- komponenty strony są zadeklarowane w wierszach 11–13
- inicjalizacja komponentu [entete] jest identyczna jak w stronach omówionych wcześniej – wiersz 17
- koszyk, który ma zostać wyświetlony, jest pobierany z sesji – wiersz 19
- wyświetlenie tego koszyka za pomocą komponentu [DataGridAchats] sprawia problemy. Trudność wynika z inicjalizacji komponentu. Przypomnijmy jego kolumny:
- kolumna [Article] powiązana z polem [nom] w źródle danych
- kolumna [Qté] powiązana z polem [qte] w źródle danych
- kolumna [Prix] powiązana z polem [prix] w źródle danych
- kolumna [Total] powiązana z polem [total] w źródle danych
Źródłem danych, którym dysponujemy, jest koszyk wraz z listą zakupów. Ta ostatnia będzie źródłem danych dla [DataGrid]. Jedynie obiekty [Achat], które będą zasilać wiersze obiektu [DataGrid], nie posiadają właściwości [nom, qte, prix, total] oczekiwanych przez obiekt [DataGrid]. Dlatego też tworzymy tutaj, specjalnie dla obiektu [DataGrid], źródło danych, którego elementy posiadają cechy oczekiwane przez obiekt [DataGrid]. Elementy te będą typu [LigneAchat] – klasy utworzonej specjalnie na tę okazję i wywodzącej się z klasy [Achat] – wiersze 36–66
- po zdefiniowaniu klasy [LigneAchat] źródło danych klasy [DataGridAchats] jest tworzone na podstawie koszyka znalezionego w sesji – wiersze 20–30
- kwota zakupów jest wyświetlana dzięki właściwości [totalPanier] klasy [Panier] – wiersz 32
1.5.6.5. Widok [paniervide.aspx]
1.5.6.5.1. Introduction
Ten widok wyświetla informację, że koszyk jest pusty:

Jest on wyświetlany po wysłaniu żądania /main?action=koszyk lub /main?action=usunąćzakup&id=ID. Elementy żądania kontrolera są następujące:
obiekt Hashtable() – tablica opcji menu |
1.5.6.5.2. Elementy strony
![]() |
n° | type | nom | rôle |
element użytkownika | nagłówek | wyświetl nagłówek |
1.5.6.5.3. Kod prezentacji [paniervide.aspx]
Uwagi:
- nagłówek znajduje się w wierszu 9
1.5.6.5.4. Kod kontrolny [paniervide.aspx.vb]
Uwagi:
- wystarczy zainicjować jedyny dynamiczny komponent strony – wiersz 10
1.5.6.6. Widok [erreurs.aspx]
1.5.6.6.1. Introduction
Ten widok jest wyświetlany w przypadku wystąpienia błędów:

Jest ona wyświetlana po każdym zapytaniu zakończonym błędem, z wyjątkiem operacji zakupu z błędną ilością, która jest obsługiwana przez widok [INFOS]. Elementy zapytania kontrolera są następujące:
obiekt Hashtable() – tablica opcji menu | |
ArrayList obiektów [String] reprezentujących komunikaty o błędach do wyświetlenia |
1.5.6.6.2. Elementy składowe strony
![]() |
n° | type | nom | rôle |
element użytkownika | nagłówek | wyświetl nagłówek | |
powtarzacz | rptErreurs | wyświetl listę błędów |
1.5.6.6.3. Kod prezentacji [erreurs.aspx]
Uwagi:
- nagłówek jest zdefiniowany w wierszu 9
- komponent [rptErreurs] jest zdefiniowany w wierszach 13–19. Jego zawartość pochodzi ze źródła danych typu [ArrayList], zawierającego obiekty typu [String].
1.5.6.6.4. Kod kontrolny [erreurs.aspx.vb]
Uwagi:
- komponent [entete] jest inicjowany w zwykły sposób, wiersze 9 i 14
- komponent [rptErreurs] jest inicjowany z listą błędów typu [ArrayList] znalezioną w kontekście – wiersze 16–19
1.5.7. Kontrolery global.asax, main.aspx
Pozostaje jeszcze napisanie rdzenia naszej aplikacji internetowej, czyli kontrolera. Jego rolą jest:
- odbioru żądania od klienta,
- przetworzenie żądanej przez niego akcji przy pomocy klas biznesowych,
- wysłanie w odpowiedzi odpowiedniego widoku.
1.5.7.1. Kontroler [global.asax.vb]
Gdy aplikacja odbiera swoje pierwsze żądanie, uruchamiana jest procedura [Application_Start] z pliku [global.asax.vb]. Stanie się to tylko raz. Celem procedury [Application_Start] jest zainicjowanie obiektów niezbędnych dla aplikacji internetowej, które będą współdzielone w trybie tylko do odczytu przez wszystkie wątki klienckie. Te współdzielone obiekty mogą być umieszczone w dwóch miejscach:
- w polach prywatnych kontrolera
- kontekście wykonania aplikacji (Application)
Metoda [Application_Start] aplikacji [global.asax.vb] wykona następujące czynności:
- sprawdzi, czy w pliku [web.config] znajdują się parametry niezbędne do prawidłowego działania aplikacji. Zostały one opisane w punkcie 1.5.3.
- umieści w kontekście aplikacji listę ewentualnych błędów w postaci obiektu [ArrayList erreurs]. Lista ta będzie pusta, jeśli nie ma błędów, ale mimo to będzie istnieć.
- jeśli wystąpiły błędy, metoda [Application_Start] kończy się w tym miejscu. W przeciwnym razie żąda ona odwołania do singletonu typu [IArticlesDomain], który będzie obiektem biznesowym wykorzystywanym przez kontroler do swoich potrzeb. Jak wyjaśniono w punkcie 1.5.3.2, kontroler zażąda tego singletonu od frameworka Spring. Ta operacja instancjonowania może spowodować różne błędy. Jeśli tak się stanie, zostaną one ponownie zapisane w obiekcie [erreurs] w kontekście aplikacji.
Kontroler [global.asax.vb] posiada procedurę [Session_Start], która jest wykonywana za każdym razem, gdy pojawia się nowy klient. W tej procedurze tworzony jest pusty koszyk dla klienta. Koszyk ten będzie utrzymywany na bieżąco w trakcie kolejnych żądań tego konkretnego klienta. Kod mógłby wyglądać następująco:
Uwagi:
- parametry oczekiwane w procedurze [web.config] są zdefiniowane w tablicy – wiersz 18
- są one wyszukiwane w [web.config]. Jeśli występują, są zapisywane w kontekście aplikacji, w przeciwnym razie błąd jest rejestrowany na liście błędów [erreurs] – wiersze 21–33
- jeśli nie ma błędów, zwraca się do Springa o odwołanie do singletonu [articlesDomain], który zarządza dostępem do warstwy [domain] aplikacji – wiersze 35–47. Ewentualne błędy są rejestrowane w [erreurs].
- Błędy są rejestrowane w kontekście aplikacji – wiersz 49
- w przypadku wystąpienia błędów procedura zostaje zakończona – wiersz 51
- tworzy się tablicę składającą się z trzech słowników. Każdy z nich ma dwa klucze: href i link. Tablica ta reprezentuje trzy możliwe opcje menu – wiersze 52–71
- tablica ta jest zapisywana w kontekście aplikacji – wiersz 73
- dla każdego nowego klienta uruchamiana jest procedura [Session_Start]. Tworzy się w niej pusty koszyk w sesji klienta – wiersze 78–81
1.5.7.2. Kontroler [main.aspx.vb]
Kontroler [main.aspx.vb] obsługuje wszystkie żądania klientów. Wszystkie mają one bowiem postać [/webarticles/main.aspx?action=XX]. Żądanie jest przetwarzane w następujący sposób:
- sprawdzany jest obiekt [erreurs] z kontekstu aplikacji. Jeśli nie jest pusty, oznacza to, że podczas inicjalizacji aplikacji wystąpiły błędy i aplikacja nie może działać. W odpowiedzi wysyłany jest wówczas widok [ERREURS].
- parametr [action] z żądania zostanie pobrany i sprawdzony. Jeśli nie odpowiada on żadnej znanej akcji, wysyłany jest widok [ERREURS] wraz z odpowiednim komunikatem o błędzie.
- Jeśli parametr [action] jest prawidłowy, żądanie klienta jest przekazywane do procedury specyficznej dla danej akcji w celu przetworzenia:
metoda | żądanie | przetwarzanie | możliwe odpowiedzi |
GET /main?action=liste | - wyświetlić listę artykułów od klasy biznesowej - wyświetlić ją | [LISTE] lub [ERREURS] | |
GET /main?action=infos&id=ID | - poproś o artykuł o identyfikatorze id=ID w klasy biznesowej - wyświetlić go | [INFOS] lub [ERREURS] | |
POST /main?action=zakup&id=ID - ilość zakupiona jest częścią przesłanych parametrów | - należy zwrócić się o artykuł o identyfikatorze id=ID do klasy biznesowej - dodaj go do koszyka w sesji klienta | [LISTE] lub [INFOS] lub [ERREURS] | |
GET /main?action=retirerachat&id=ID | - usunąć produkt o identyfikatorze id=ID z listy zakupów w koszyku sesji klienta | [PANIER] | |
GET /main?action=koszyk | - wyświetl koszyk z sesji klienta | [PANIER] lub [PANIERVIDE] | |
GET /main?action=validationpanier | - zmniejszyć w bazie danych stanów magazynowych wszystkich artykułów znajdujących się w koszyku sesji klienta | [LISTE] lub [ERREURS] |
Szkielet kontrolera [main.aspx.vb] mógłby wyglądać następująco:
Uwagi:
- klasa posiada dwa pola prywatne, które będą współdzielone przez metody – wiersze 15–16:
- articlesDomain: singleton zapewniający dostęp do warstwy [domain]
- opcje: tablica słowników opcji menu
- procedura [Page_Load]:
- zainicjuje dwa pola prywatne klasy
- pobierze parametr [action] z zapytania i uruchomi metodę przetwarzającą tę akcję.
1.5.7.3. Metoda [Page_Load]
To zdarzenie jest pierwszym, które ma miejsce na stronie. Kod wygląda następująco:
Uwagi:
- przy każdym ładowaniu strony sprawdzamy, czy inicjalizacja aplikacji przeprowadzona przez metodę [global.asax] przebiegła pomyślnie.
- W tym celu pobieramy z kontekstu aplikacji listę błędów umieszczoną tam przez [global.asax] – wiersz 4
- jeśli lista ta nie jest pusta, wyświetla się widok [ERREURS] – wiersze 6–10
- pobieramy singleton [articlesDomain] umieszczony przez [global.asax] w kontekście aplikacji i zapisujemy go w polu prywatnym [articlesDomain], aby był dostępny dla różnych metod klasy – wiersz 12
- wykonujemy podobną operację z tablicą opcji menu – wiersz 14
- pobieramy parametr [action] z żądania – wiersz 16
- wywołujemy metodę odpowiadającą żądanej akcji. Akcja nieprzewidziana jest traktowana jako akcja [liste] – wiersze 16–36
1.5.7.4. Obsługa akcji [liste]
Chodzi o wyświetlenie listy artykułów:

Kod jest następujący:
Uwagi:
- ewentualne błędy umieszcza się w pliku [ArrayList] – wiersz 4
- lista artykułów jest pobierana z singletonu [articlesDomain] – wiersze 5–12
- jeśli wystąpiły błędy, wysyłany jest widok [ERREURS] – wiersze 13–19
- w przeciwnym razie wysyłany jest widok [LISTE] – wiersze 20–24
1.5.7.5. Przetwarzanie akcji [infos]
Klient poprosił o informacje dotyczące danego artykułu:

Kod jest następujący:
Uwagi:
- ewentualne błędy umieszczamy w [ArrayList] – wiersz 4
- identyfikator żądanego artykułu jest pobierany z zapytania – wiersz 6
- identyfikator ten jest sprawdzany. Musi być obecny i musi to być liczba całkowita. Jeśli tak nie jest, wysyłany jest widok [ERREURS] wraz z odpowiednim komunikatem o błędzie – wiersze 7–27
- po zweryfikowaniu identyfikatora artykuł jest pobierany z singletonu [articlesDomain]. Jeśli wystąpi wyjątek, wysyłany jest widok [ERREURS] – wiersze 29–39
- jeśli artykuł nie został znaleziony, wysyłany jest widok [ERREURS] – wiersze 41–48
- jeśli artykuł został znaleziony, jest on umieszczany w sesji użytkownika, a następnie wyświetlany w widoku [INFOS] – wiersze 50–56
1.5.7.6. Przetwarzanie akcji [achat]
Klient kupił artykuł wyświetlony w widoku [INFOS].

Kod wygląda następująco:
Uwagi:
- artykuł zapisany w sesji jest pobierany – wiersz 5
- jeśli go tam nie ma (sesja mogła wygasnąć), wyświetlany jest widok [LISTE] – wiersze 7–10
- ilość zakupiona jest pobierana z zapytania – wiersz 12
- sprawdzana jest jego ważność – wiersze 13–29
- w przypadku nieprawidłowości, w zależności od sytuacji, wysyłany jest widok [LISTE] – wiersz 16 lub widok [INFOS] – wiersze 24–28
- jeśli wszystko jest w porządku, zakup jest zapisywany w koszyku – wiersze 31–32
- następnie wysyłany jest widok [LISTE] – wiersz 34
1.5.7.7. Przetwarzanie akcji [panier]
Klient dokonał kilku zakupów i prosi o wyświetlenie koszyka:

Kod wygląda następująco:
Uwagi:
- koszyk pobieramy z sesji – wiersz 4. Nie sprawdzamy tutaj, czy faktycznie coś otrzymaliśmy. Należałoby to zrobić, ponieważ sesja mogła wygasnąć.
- Jeśli koszyk jest pusty, wysyłamy widok [PANIERVIDE] – wiersze 6–10
- w przeciwnym razie wysyłamy widok [PANIER] – wiersze 11–14
1.5.7.8. Przetwarzanie akcji [retirerachat]
Klient chce usunąć produkt z koszyka:

Kod jest następujący:
Uwagi:
- pobieramy zawartość koszyka z sesji – wiersz 4. Nie sprawdzamy tutaj, czy faktycznie coś udało się pobrać. Należałoby to zrobić, ponieważ sesja mogła wygasnąć.
- W zapytaniu pobieramy identyfikator [id] produktu, który ma zostać usunięty – wiersz 8.
- Odpowiedni zakup jest usuwany z koszyka – wiersz 10
- W tym miejscu nie sprawdzono poprawności identyfikatora zakupionego produktu. Jeśli ma on nieprawidłowy typ, wystąpi wyjątek, który zostanie obsłużony w wierszach 11–13. Jeśli jest poprawny, ale produkt nie istnieje, metoda [panier.enlever] – wiersz 10 – nie wykonuje żadnej czynności.
- wyświetlany jest nowy koszyk – wiersz 16
1.5.7.9. Przetwarzanie akcji [validerpanier]
Klient chce zatwierdzić swój koszyk:

Kod wygląda następująco:
Uwagi:
- koszyk pobieramy z sesji – wiersz 6. Nie sprawdzamy tutaj, czy faktycznie coś otrzymaliśmy. Należałoby to zrobić, ponieważ sesja mogła wygasnąć.
- W przypadku wygaśnięcia sesji będziemy mieli wskaźnik [nothing] dla koszyka, a metoda [acheter] – wiersz 9 – wygeneruje wyjątek i zostanie wysłany widok [ERREURS]. Jednak komunikat o błędzie będzie mało zrozumiały dla użytkownika.
- W wierszach 8–16 próbujemy zweryfikować koszyk pobrany z sesji. Niektóre zakupy mogą nie zostać zatwierdzone, jeśli zamówiona ilość przekracza stan magazynowy danego artykułu. Przypadki te są zapisywane przez metodę [acheter] na liście błędów, która jest pobierana w wierszu 11.
- Jeśli wystąpią błędy, wysyłany jest widok [ERREURS] – wiersze 18–23
- w przeciwnym razie wysyłany jest widok [LISTE] – wiersze 25–26
1.6. Conclusion
Opracowaliśmy tutaj aplikację zgodnie z wzorcem MVC. Wydaje się, że (w kwietniu 2005 r.) nie istnieją profesjonalne „frameworki” do tworzenia aplikacji w języku MVC, podobne do tych dostępnych w Javie (Struts, Spring, ...). Projekt [Spring.net] powinien wkrótce zaproponować taki framework. W oczekiwaniu na to wydarzenie powyższa metoda umożliwia realne tworzenie aplikacji MVC o średniej wielkości.































