1. Teil 1
Das PDF des Dokuments ist |HIER| verfügbar.
Die Beispiele im Dokument sind |HIER| verfügbar.
1.1. Einleitung
Ziele dieses Artikels:
- Eine dreischichtige Webanwendung schreiben [Benutzeroberfläche, Geschäftslogik, Datenzugriff]
- Konfigurieren der Anwendung mit Spring IOC
- Erstellen verschiedener Versionen durch Änderung der Implementierung einer oder mehrerer der drei Schichten.
Verwendete Tools:
- Visual Studio.NET für die Entwicklung – siehe Anhang, Abschnitt 3.1;
- Cassini-Webserver zur Ausführung – siehe Anhang, Abschnitt 3.2;
- Nunit für Unit-Tests – siehe Anhang, Abschnitt 3.4;
- Spring für die Integration und Konfiguration der Webanwendungsschichten – siehe Anhang, Abschnitt 3.3;
Auf einer Skala von Anfänger über Fortgeschrittene bis hin zu Experten fällt dieses Dokument in die Kategorie [Fortgeschrittene bis Experten]. Um es zu verstehen, sind verschiedene Voraussetzungen erforderlich. Einige davon finden sich in Dokumenten, die ich verfasst habe. In solchen Fällen verweise ich darauf. Es versteht sich von selbst, dass dies lediglich ein Vorschlag ist und es dem Leser freisteht, seine bevorzugten Ressourcen zu nutzen.
- VB.NET-Sprache: [Einführung in VB.NET anhand von Beispielen];
- Webprogrammierung in VB.NET: [Webentwicklung mit ASP.NET 1.1];
- Verwendung des IoC-Aspekts von Spring: [Spring IoC für .NET];
- Spring.NET-Dokumentation: [Spring.NET | Startseite]
Dieses Dokument folgt derselben Struktur wie ein für Java verfasstes Dokument [3-Schichten-Architekturen und MVC-Architekturen mit Struts, Spring und Java]. Wir erstellen die in Java geschriebene dreischichtige MVC-Webanwendung mit VB.NET. Damit möchten wir verdeutlichen, dass die Entwicklungsplattformen Java und .NET so ähnlich sind, dass in einem dieser beiden Bereiche erworbene Kenntnisse im anderen wiederverwendet werden können.
Es scheint keine allgemein anerkannte ASP.NET-MVC-Entwicklungslösung zu geben. Die folgende Lösung greift die im Dokument [Web Development with ASP.NET 1.1] vorgestellte Methode auf. Diese Methode hat zwar den Vorteil, dass sie in der J2EE-Entwicklung gängige Konzepte nutzt, sollte aber dennoch als das betrachtet werden, was sie ist: eine von vielen MVC-Entwicklungsmethoden. Sobald sich eine MVC-Entwicklungsmethode in ASP.NET allgemein durchgesetzt hat, sollte sie übernommen werden. Die derzeit in Entwicklung befindliche .NET-Version von Spring könnte durchaus eine erste Lösung sein.
1.2. Die Webarticles-Anwendung
Hier stellen wir die Komponenten einer vereinfachten E-Commerce-Webanwendung vor. Diese Anwendung ermöglicht es Webnutzern:
- eine Liste von Artikeln aus einer Datenbank anzeigen
- einige davon in einen Online-Warenkorb legen
- den Warenkorb zu bestätigen. Diese Bestätigung aktualisiert lediglich den Bestand der gekauften Artikel in der Datenbank.
Dem Benutzer werden folgende Ansichten angezeigt:
- die Ansicht „LIST“, die eine Liste der zum Verkauf stehenden Artikel anzeigt ![]() | - die Ansicht [INFO], die zusätzliche Informationen zu einem Produkt bereitstellt: ![]() |
- die Ansichten [WARENKORB] und [LEERER WARENKORB], die den Inhalt des Warenkorbs des Kunden anzeigen
![]() | ![]() |
- die Ansicht [ERRORS], die etwaige Anwendungsfehler meldet

1.3. Allgemeine Anwendungsarchitektur
Wir möchten eine Anwendung mit der folgenden dreischichtigen Struktur erstellen:
![]() |
- Die drei Schichten werden durch die Verwendung von Schnittstellen voneinander unabhängig gemacht
- Die Integration der verschiedenen Schichten wird von Spring übernommen
- Jede Schicht hat ihren eigenen Namensraum: web (UI-Schicht), domain (Geschäftsschicht) und dao (Datenzugriffsschicht).
Die Anwendung folgt einer MVC-Architektur (Model-View-Controller). Wenn wir uns das obige Schichtdiagramm noch einmal ansehen, fügt sich die MVC-Architektur wie folgt ein:
![]() |
Die Bearbeitung einer Client-Anfrage erfolgt in folgenden Schritten:
- Der Client sendet eine Anfrage an den Controller. In diesem Fall ist der Controller eine .aspx-Seite, die eine bestimmte Rolle spielt. Sie verarbeitet alle Client-Anfragen. Sie ist der Einstiegspunkt der Anwendung. Sie ist das C in MVC.
- Der Controller verarbeitet diese Anfrage. Dazu benötigt er möglicherweise Unterstützung durch die Geschäftslogik, die als „M“ in der MVC-Struktur bezeichnet wird.
- Der Controller erhält eine Antwort von der Geschäftsschicht. Die Anfrage des Clients wurde verarbeitet. Dies kann verschiedene mögliche Antworten auslösen. Ein klassisches Beispiel ist
- eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
- ansonsten eine Bestätigungsseite
- Der Controller wählt die Antwort (= Ansicht) aus, die an den Client gesendet werden soll. Meistens handelt es sich dabei um eine Seite mit dynamischen Elementen. Der Controller stellt diese der Ansicht zur Verfügung.
- Die Ansicht wird an den Client gesendet. Dies ist das V in MVC.
1.4. Das Modell
Hier betrachten wir das M in MVC. Das Modell besteht aus den folgenden Elementen:
- Geschäftsklassen
- Datenzugriffsklassen
- die Datenbank
1.4.1. Die Datenbank
Die Datenbank enthält nur eine Tabelle namens ARTICLES. Diese Tabelle wurde mit den folgenden SQL-Befehlen generiert:
CREATE TABLE ARTICLES (
ID INTEGER NOT NULL,
NOM VARCHAR(20) NOT NULL,
PRIX NUMERIC(15,2) NOT NULL,
STOCKACTUEL INTEGER NOT NULL,
STOCKMINIMUM INTEGER NOT NULL
);
/* constraints */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>=0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
/* primary key */
ALTER TABLE ARTICLES ADD CONSTRAINT PK_ARTICLES PRIMARY KEY (ID);
Primärschlüssel, der einen Artikel eindeutig identifiziert | |
Elementname | |
sein Preis | |
aktueller Lagerbestand | |
der Lagerbestand, unterhalb dessen eine Nachbestellung aufgegeben werden muss |
1.4.2. Die Namespaces des Modells
Modell M wird hier in Form von zwei Namensräumen bereitgestellt:
- istia.st.articles.dao: enthält die Datenzugriffsklassen der [dao]-Schicht
- istia.st.articles.domain: enthält die Business-Klassen der [domain]-Schicht
Jeder dieser Namespaces wird in einer eigenen „Assembly“-Datei generiert:
Inhalt | role | |
- [IArticlesDao]: die Schnittstelle für den Zugriff auf die [dao]-Schicht. Dies ist die einzige Schnittstelle, die für die [domain]-Schicht sichtbar ist. Sie sieht keine anderen. - [Article]: Klasse, die einen Artikel definiert - [ArticlesDaoArrayList]: Implementierungsklasse der der Schnittstelle [IArticlesDao] mit einer [ArrayList] | Datenzugriffsebene - befindet sich vollständig innerhalb der [DAO]-Schicht der 3-Tier-Architektur der der Webanwendung | |
- [IArticlesDomain]: die Schnittstelle für den Zugriff auf die [domain]-Schicht. Dies ist die einzige Schnittstelle, die für die Webschicht sichtbar ist. Sie sieht keine anderen. - [AchatsArticles]: eine Klasse, die [IArticlesDomain] implementiert - [Purchase]: eine Klasse, die den Einkauf eines Kunden repräsentiert - [Cart]: eine Klasse, die die Gesamteinkäufe eines Kunden repräsentiert | stellt das Modell der Web-Einkäufe dar Web – befindet sich vollständig in der [Domain]-Schicht der 3-Schichten-Architektur der Webanwendung |
1.4.3. Die [DAO]-Schicht
Die [DAO]-Schicht enthält die folgenden Elemente:
-
[IArticlesDao]: die Schnittstelle für den Zugriff auf die [dao]-Schicht
-
[Article]: Klasse, die einen Artikel definiert
-
[ArticlesDaoArrayList]: Implementierungsklasse für die [IArticlesDao]-Schnittstelle unter Verwendung einer [ArrayList]-Klasse
Die [Visual Studio]-Projektstruktur für die [dao]-Schicht sieht wie folgt aus:

Kommentare:
- Das [dao]-Projekt ist vom Typ [Klassenbibliothek]
- Die Klassen wurden in einer Baumstruktur ab dem Ordner [istia] angeordnet. Sie befinden sich alle im Namespace [istia.st.articles.dao].
1.4.3.1. Die Klasse [Article]
Die Klasse, die einen Artikel definiert, sieht wie folgt aus:
Imports System
Namespace istia.st.articles.dao
Public Class Article
' private fields
Private _id As Integer
Private _nom As String
Private _prix As Double
Private _stockactuel As Integer
Private _stockminimum As Integer
' id article
Public Property id() As Integer
Get
Return _id
End Get
Set(ByVal Value As Integer)
If Value <= 0 Then
Throw New Exception("Le champ id [" + Value.ToString + "] est invalide")
End If
Me._id = Value
End Set
End Property
' item name
Public Property nom() As String
Get
Return _nom
End Get
Set(ByVal Value As String)
If Value Is Nothing OrElse Value.Trim.Equals("") Then
Throw New Exception("Le champ nom [" + Value + "] est invalide")
End If
Me._nom = Value
End Set
End Property
' item price
Public Property prix() As Double
Get
Return _prix
End Get
Set(ByVal Value As Double)
If Value < 0 Then
Throw New Exception("Le champ prix [" + Value.ToString + "] est invalide")
End If
Me._prix = Value
End Set
End Property
' current stock item
Public Property stockactuel() As Integer
Get
Return _stockactuel
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Le champ stockActuel [" + Value.ToString + "] est invalide")
End If
Me._stockactuel = Value
End Set
End Property
' minimum stock item
Public Property stockminimum() As Integer
Get
Return _stockminimum
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Le champ stockMinimum [" + Value.ToString + "] est invalide")
End If
Me._stockminimum = Value
End Set
End Property
' default builder
Public Sub New()
End Sub
' builder with properties
Public Sub New(ByVal id As Integer, ByVal nom As String, ByVal prix As Double, ByVal stockactuel As Integer, ByVal stockminimum As Integer)
Me.id = id
Me.nom = nom
Me.prix = prix
Me.stockactuel = stockactuel
Me.stockminimum = stockminimum
End Sub
' article identification method
Public Overrides Function ToString() As String
Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
End Function
End Class
End Namespace
Diese Klasse bietet:
- einen Konstruktor zum Festlegen der 5 Informationen für einen Artikel: [id, name, price, currentStock, minimumStock]
- öffentliche Eigenschaften zum Lesen und Schreiben der 5 Informationen
- eine Validierung der für den Artikel eingegebenen Daten. Sind die Daten ungültig, wird eine Ausnahme ausgelöst.
- Eine toString-Methode, die den Wert eines Elements als Zeichenfolge zurückgibt. Dies ist häufig nützlich für die Fehlersuche in einer Anwendung.
1.4.3.2. Die Schnittstelle [IArticlesDao]
Die Schnittstelle [IArticlesDao] ist wie folgt definiert:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Interface IArticlesDao
' list of all items
Function getAllArticles() As IList
' add an article
Function ajouteArticle(ByVal unArticle As Article) As Integer
' deletes an article
Function supprimeArticle(ByVal idArticle As Integer) As Integer
' modify an article
Function modifieArticle(ByVal unArticle As Article) As Integer
' search for an article
Function getArticleById(ByVal idArticle As Integer) As Article
' deletes all articles
Sub clearAllArticles()
' changes the stock of an item
Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
End Interface
End Namespace
Die verschiedenen Methoden der Schnittstelle haben folgende Funktionen:
gibt alle Elemente aus der Datenquelle zurück | |
löscht die Datenquelle | |
gibt das [Article]-Objekt zurück, das durch seinen Primärschlüssel identifiziert wird | |
ermöglicht es Ihnen, einen Artikel zur Datenquelle hinzuzufügen | |
ermöglicht es Ihnen, einen Artikel in der Datenquelle zu ändern | |
ermöglicht es Ihnen, einen Eintrag aus der Datenquelle zu löschen | |
ermöglicht es Ihnen, den Lagerbestand eines Artikels in der Datenquelle zu ändern |
Die Schnittstelle stellt Client-Programmen eine Reihe von Methoden zur Verfügung, die ausschließlich durch ihre Signaturen definiert sind. Sie befasst sich nicht damit, wie diese Methoden tatsächlich implementiert werden. Dies sorgt für Flexibilität in einer Anwendung. Das Client-Programm ruft eine Schnittstelle auf, anstatt eine bestimmte Implementierung dieser Schnittstelle anzusprechen.
![]() |
Die Auswahl einer bestimmten Implementierung erfolgt über eine Spring-Konfigurationsdatei. Um zu veranschaulichen, dass für das Testen der Webanwendung nur die Datenzugriffsschnittstelle von Bedeutung ist – und nicht deren Implementierungsklasse –, werden wir die Datenquelle zunächst mithilfe eines einfachen [ArrayList]-Objekts implementieren. Später werden wir eine datenbankbasierte Lösung vorstellen.
1.4.3.3. Die Implementierungsklasse [ArticlesDaoArrayList]
Die Implementierungsklasse [ArticlesDaoArrayList] ist wie folgt definiert:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Class ArticlesDaoArrayList
Implements istia.st.articles.dao.IArticlesDao
Private articles As New ArrayList
Private Const nbArticles As Integer = 4
' default builder
Public Sub New()
' we build a few items
For i As Integer = 1 To nbArticles
articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
Next
End Sub
' list of all items
Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
' returns the list of items
SyncLock Me
Return articles
End SyncLock
End Function
' delete all items
Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
' empty the list of items
SyncLock Me
articles.Clear()
End SyncLock
End Sub
' obtain an item identified by its key
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
' search for the item in the list
SyncLock Me
Dim ipos As Integer = posArticle(articles, idArticle)
If ipos <> -1 Then
Return CType(articles(ipos), Article)
Else
Return Nothing
End If
End SyncLock
End Function
' add an item to the list of items
Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
' add the item to the list of items
SyncLock Me
' we check that it doesn't already exist
Dim ipos As Integer = posArticle(articles, unArticle.id)
If ipos <> -1 Then
Throw New Exception("L'article d'id [" + unArticle.id.ToString + "] existe déjà")
End If
' we add the article
articles.Add(unArticle)
' we return the result
Return 1
End SyncLock
End Function
' modify an article
Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
' modify an article
SyncLock Me
' we check that
Dim ipos As Integer = posArticle(articles, articleNouveau.id)
' if it doesn't exist
If ipos = -1 Then Return 0
' it exists - we modify it
articles(ipos) = articleNouveau
' we return the result
Return 1
End SyncLock
End Function
' delete an item identified by its key
Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
' article deletion
SyncLock Me
' we check that
Dim ipos As Integer = posArticle(articles, idArticle)
' if it doesn't exist
If ipos = -1 Then Return 0
' it exists - we remove it
articles.RemoveAt(ipos)
' we return the result
Return 1
End SyncLock
End Function
' change the stock of an item identified by its key
Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
' change the stock of an item
SyncLock Me
' we check that
Dim ipos As Integer = posArticle(articles, idArticle)
' if it doesn't exist
If ipos = -1 Then Return 0
' it exists - you modify your stock if you can
Dim unArticle As Article = CType(articles(ipos), Article)
' only change stock if it is sufficient
If unArticle.stockactuel + mouvement >= 0 Then
unArticle.stockactuel += mouvement
Return 1
Else
Return 0
End If
End SyncLock
End Function
' search for an item identified by its key
Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
' returns the position of item [idArticle] in the list or -1 if not found
Dim unArticle As Article
For i As Integer = 0 To listArticles.Count - 1
unArticle = CType(listArticles(i), Article)
If unArticle.id = idArticle Then
Return i
End If
Next
' not found
Return -1
End Function
End Class
End Namespace
Anmerkungen:
- Die Datenquelle wird durch das private Feld [articles] vom Typ [ArrayList] simuliert
- Der Klassenkonstruktor erstellt standardmäßig 4 Elemente in der Datenquelle.
- Alle Datenzugriffsmethoden wurden synchronisiert, um Probleme durch gleichzeitigen Zugriff auf die Datenquelle zu vermeiden. Zu jedem Zeitpunkt hat nur ein Thread Zugriff auf eine bestimmte Methode.
- Die Methode [posArticle] gibt die Position [0..N] in der [ArrayList]-Datenquelle eines durch seine Nummer identifizierten Artikels zurück. Wenn der Artikel nicht existiert, gibt die Methode die Position -1 zurück. Diese Methode wird von den anderen Methoden wiederholt verwendet.
- Die Methode [addArticle] fügt einen Artikel zur Liste der Artikel hinzu. Sie gibt die Anzahl der eingefügten Artikel zurück: 1. Wenn der Artikel bereits existierte, wird eine Ausnahme ausgelöst.
- Die Methode [modifyItem] ermöglicht es Ihnen, ein vorhandenes Element zu ändern. Sie gibt die Anzahl der geänderten Elemente zurück: 1, wenn das Element vorhanden war, andernfalls 0.
- Die Methode [deleteArticle] löscht einen vorhandenen Artikel. Sie gibt die Anzahl der gelöschten Artikel zurück: 1, wenn der Artikel vorhanden war, andernfalls 0.
- Die Methode [getAllArticles] gibt eine Liste aller Artikel zurück
- Die Methode [getArticleById] ruft einen Artikel anhand seiner ID ab. Sie gibt den Wert [nothing] zurück, wenn der Artikel nicht existiert.
- Der Code stellt keine wirkliche Schwierigkeit dar. Wir überlassen es dem Leser, ihn durchzugehen und zu verstehen.
1.4.3.4. Erstellen der [dao]-Layer-Assembly
Das Visual Studio-Projekt ist so konfiguriert, dass die Assembly [webarticles-dao.dll] generiert wird. Diese wird im Ordner [bin] des Projekts generiert:
![]() | ![]() |
1.4.3.5. NUnit-Tests für die [dao]-Schicht
In Java werden Klassen mithilfe des [JUnit]-Frameworks getestet. In .NET bietet das NUnit-Framework die gleichen Unit-Test-Funktionen:

Das Visual Studio-Testprojekt ist wie folgt aufgebaut:

Anmerkungen:
- Das [tests]-Projekt ist eine [Class Library]
- [NUnit]-Tests erfordern einen Verweis auf die Assembly [nunit.framework.dll]
- Die [NUnit]-Testklasse ruft über Spring eine Instanz des zu testenden Objekts ab. Daher
- müssen im Ordner [bin] die Spring-Klassendateien
- in [References] muss ein Verweis auf die [Spring-Core.dll]-Assembly aus dem [bin]-Ordner
- in [bin] eine Konfigurationsdatei für Spring
- Die Testklasse benötigt die Assembly [webarticles-dao.dll] aus der [dao]-Schicht. Diese wurde im Ordner [bin] abgelegt und ihr Verweis zu den Projektverweisen hinzugefügt.
Eine [NUnit]-Testklasse benötigt Zugriff auf Klassen im Namespace [NUnit.Framework]. Daher ist die folgende Import-Anweisung enthalten:
Der Namespace [NUnit.Framework] befindet sich in der Assembly [nunit.framework.dll], die zu den Projektreferenzen hinzugefügt werden muss:
![]() | ![]() |
Die Assembly [nunit.framework.dll] sollte in der angezeigten Liste enthalten sein, wenn [NUnit] installiert wurde. Doppelklicken Sie einfach auf die Assembly, um sie dem Projekt hinzuzufügen:

Die [NUnit]-Testklasse für die [DAO]-Schicht könnte wie folgt aussehen:
Imports System
Imports System.Collections
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports System.Threading
Imports Spring.Objects.Factory.Xml
Imports System.IO
Namespace istia.st.articles.tests
<TestFixture()> _
Public Class NunitTestArticlesArrayList
' the test object
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' retrieve an instance of the Spring object manufacturer
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' request instantiation of articlesdao object
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
End Sub
<Test()> _
Public Sub testGetAllArticles()
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testClearAllArticles()
' delete all articles
articlesDao.clearAllArticles()
' all articles are requested
Dim articles As IList = articlesDao.getAllArticles
' verification: there must be 0
Assert.AreEqual(0, articles.Count)
End Sub
<Test()> _
Public Sub testAjouteArticle()
' delete all items
articlesDao.clearAllArticles()
' check: the item table must be empty
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' we add two items
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check: there must be two items
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testSupprimeArticle()
' delete all items
articlesDao.clearAllArticles()
' check: the item table must be empty
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' we add two items
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check: there must be 2 items
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' we delete article 4
articlesDao.supprimeArticle(4)
' check: there must be 1 item left
articles = articlesDao.getAllArticles
Assert.AreEqual(1, articles.Count)
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testModifieArticle()
' delete all items
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' article 3 search
Dim unArticle As Article = articlesDao.getArticleById(3)
' check
Assert.AreEqual(unArticle.nom, "article3")
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.nom, "article4")
' modification article 4
articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
' check
unArticle = articlesDao.getArticleById(4)
Assert.AreEqual(unArticle.prix, 44, 0.000001)
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testGetArticleById()
' article deletion
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' research article 3
Dim unArticle As Article = articlesDao.getArticleById(3)
' check
Assert.AreEqual(unArticle.nom, "article3")
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.nom, "article4")
End Sub
' screen listing
Private Sub listArticles()
Dim articles As IList = articlesDao.getAllArticles
For i As Integer = 0 To articles.Count - 1
Console.WriteLine(CType(articles(i), Article).ToString)
Next
End Sub
<Test()> _
Public Sub testArticleAbsent()
' delete all items
articlesDao.clearAllArticles()
' research article 1
Dim article As article = articlesDao.getArticleById(1)
' check
Assert.IsNull(article)
' modification of a non-existent item
Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
' had to modify no line
Assert.AreEqual(i, 0)
' deletion of non-existent item
i = articlesDao.supprimeArticle(1)
' had to delete no line
Assert.AreEqual(0, i)
End Sub
<Test()> _
Public Sub testChangerStockArticle()
' delete all items
articlesDao.clearAllArticles()
' add an item
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' add an item
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' creation of 100 threads
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' create thread i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' set the thread name
taches(i).Name = "tache_" & i
' start execution of thread i
taches(i).Start()
Next
' wait for all threads to finish
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' checks - item 3 must have a stock of 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' item 4 stock is decremented
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' check: its stock must not have changed
Assert.AreEqual(0, nbLignes)
' visual check
listArticles()
End Sub
Public Sub décrémente()
' thread launched
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' thread decrements stock
articlesDao.changerStockArticle(3, -1)
' thread terminated
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
End Class
End Namespace
Kommentare:
- Wir wollten ein Testprogramm für die Schnittstelle [IArticlesDao] schreiben, das unabhängig von ihrer Implementierungsklasse ist. Daher haben wir Spring verwendet, um den Namen der Implementierungsklasse vor dem Testprogramm zu verbergen.
- Die Methode <Setup()> ruft aus Spring eine Referenz auf das zu testende [articlesdao]-Objekt ab. Dies ist in der folgenden [spring-config.xml]-Datei definiert:
<?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>
Diese Datei gibt den Namen der Implementierungsklasse [istia.st.articles.dao.ArticlesDaoArrayList] für die Schnittstelle [IArticlesDao] an und wo diese zu finden ist [webarticles-dao.dll]. Da die Instanziierung keine Parameter erfordert, sind hier keine definiert.
- Die meisten Tests sind leicht verständlich. Dem Leser wird empfohlen, die Kommentare zu lesen.
- Die Methode [testChangerStockArticle] bedarf einer Erläuterung. Sie erstellt 100 Threads, die für die Verringerung des Lagerbestands eines bestimmten Artikels zuständig sind.
<Test()> _
Public Sub testChangerStockArticle()
' delete all items
articlesDao.clearAllArticles()
' add an item
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' add an item
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' creation of 100 threads
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' create thread i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' set the thread name
taches(i).Name = "tache_" & i
' start execution of thread i
taches(i).Start()
Next
' wait for all threads to finish
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' checks - item 3 must have a stock of 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' item 4 stock is decremented
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' check: its stock must not have changed
Assert.AreEqual(0, nbLignes)
' visual check
listArticles()
End Sub
Dies dient dazu, den gleichzeitigen Zugriff auf die Datenquelle zu testen. Die Methode, die für die Aktualisierung des Lagerbestands zuständig ist, lautet wie folgt:
Public Sub décrémente()
' thread launched
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' thread decrements stock
articlesDao.changerStockArticle(3, -1)
' thread terminated
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
Dadurch wird der Lagerbestand von Artikel Nr. 3 um eins verringert. Wenn wir uns den Code der Methode [testChangerStockArticle] ansehen, stellen wir fest, dass:
- der Lagerbestand von Artikel Nr. 3 auf 101 initialisiert ist
- die 100 Threads verringern diesen Bestand jeweils um eins
- wir sollten daher am Ende der Ausführung aller Threads einen Bestand von 1 haben
Außerdem versuchen wir, ebenfalls innerhalb dieser Methode, den Bestand von Artikel Nr. 4 auf einen negativen Wert zu setzen. Dies sollte fehlschlagen.
Um die [dao]-Schicht zu testen, generieren wir die DLL [tests-webarticles-dao.dll] im Ordner [bin] des Projekts [tests]:
![]() | ![]() |
Anschließend laden wir diese DLL mit der Anwendung [Nunit-Gui] und führen die Tests aus:

Im linken Fenster sehen wir die Liste der getesteten Methoden. Die Farbe des Punktes vor dem Namen jeder Methode zeigt an, ob die Methode bestanden (grün) oder nicht bestanden (rot) hat. Leser, die dieses Dokument am Bildschirm betrachten, werden sehen, dass alle Tests bestanden wurden. Wir betrachten die [dao]-Schicht daher als funktionsfähig.
1.4.4. Die [domain]-Schicht
Die [domain]-Schicht enthält die folgenden Elemente:
-
[IArticlesDomain]: die Schnittstelle für den Zugriff auf die [domain]-Schicht
-
[Purchase]: Klasse, die einen Kauf definiert
-
[ShoppingCart]: Klasse, die einen Warenkorb definiert
-
[ProductPurchases]: die Klasse, die die Schnittstelle [IArticlesDomain] implementiert
Die Struktur der [Visual Studio]-Lösung für die [Domain]-Schicht ist wie folgt:

Kommentare:
- Das [Domain]-Projekt ist vom Typ [Klassenbibliothek]
- Die Klassen wurden in einer Baumstruktur ab dem Ordner [istia] angeordnet. Sie befinden sich alle im Namespace [istia.st.articles.domain].
- Die DLL der [dao]-Schicht wurde im Ordner [bin] des neuen Projekts abgelegt. Zusätzlich wurde diese DLL als Referenz zum Projekt hinzugefügt.
1.4.4.1. Die Schnittstelle [IArticlesDomain]
Die Schnittstelle [IArticlesDomain] entkoppelt die [Business]-Schicht von der [Web]-Schicht. Letztere greift über diese Schnittstelle auf die [Business/Domain]-Schicht zu, ohne sich um die Klasse zu kümmern, die sie tatsächlich implementiert. Die Schnittstelle definiert die folgenden Aktionen für den Zugriff auf die Business-Schicht:
Imports Article = istia.st.articles.dao.Article
Namespace istia.st.articles.domain
Public Interface IArticlesDomain
' methods
Sub acheter(ByVal panier As Panier)
Function getAllArticles() As IList
Function getArticleById(ByVal idArticle As Integer) As Article
ReadOnly Property erreurs() As ArrayList
End Interface
End Namespace
gibt die Liste der [Article]-Objekte aus der zugehörigen Datenquelle zurück | |
gibt das durch [idArticle] identifizierte [Article]-Objekt zurück | |
bearbeitet den Warenkorb des Kunden, indem der Lagerbestand der gekauften Artikel um die gekaufte Menge verringert wird – kann fehlschlagen, wenn der Lagerbestand nicht ausreicht | |
gibt die Liste der aufgetretenen Fehler zurück – leer, wenn keine Fehler vorliegen |
1.4.4.2. Die Klasse [Purchase]
Die Klasse [Purchase] repräsentiert einen Kundenkauf:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class Achat
' private fields
Private _article As article
Private _qte As Integer
' default builder
Public Sub New()
End Sub
' builder with parameters
Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
' we go through the properties
Me.article = unArticle
Me.qte = qte
End Sub
' item purchased
Public Property article() As article
Get
Return _article
End Get
Set(ByVal Value As article)
_article = Value
End Set
End Property
' qty purchased
Public Property qte() As Integer
Get
Return _qte
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Quantité [" + Value.ToString + "] invalide")
End If
_qte = Value
End Set
End Property
' total purchase
Public ReadOnly Property totalAchat() As Double
Get
Return _qte * _article.prix
End Get
End Property
' identity
Public Overrides Function ToString() As String
Return "[" + _article.ToString + "," + _qte.ToString + "]"
End Function
End Class
End Namespace
Kommentare:
- Die Klasse [Purchase] verfügt über die folgenden Eigenschaften und Methoden:
der gekaufte Artikel | |
die gekaufte Menge | |
der Kaufbetrag | |
Zeichenkettendarstellung des Objekts |
- Sie verfügt über einen Konstruktor, der die Eigenschaften [item, qty] initialisiert, die einen Kauf definieren.
1.4.4.3. Die Klasse [Cart]
Die Klasse [Cart] repräsentiert die gesamten Einkäufe des Kunden:
Namespace istia.st.articles.domain
Public Class Panier
' private fields
Private _achats As New ArrayList
Private _totalPanier As Double = 0
' default builder
Public Sub New()
End Sub
' list of purchases
Public ReadOnly Property achats() As ArrayList
Get
Return _achats
End Get
End Property
' total purchases
Public ReadOnly Property totalPanier() As Double
Get
Return _totalPanier
End Get
End Property
' methods
Public Sub ajouter(ByVal unAchat As Achat)
' find out if the purchase already exists
Dim iAchat As Integer = posAchat(unAchat.article.id)
If iAchat <> -1 Then
' we found
Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
achatCourant.qte += unAchat.qte
Else
' we didn't find
_achats.Add(unAchat)
End If
' increment the basket total
_totalPanier += unAchat.totalAchat
End Sub
' remove a purchase
Public Sub enlever(ByVal idAchat As Integer)
' we're looking to buy
Dim iachat As Integer = posAchat(idAchat)
' if found, remove
If iachat <> -1 Then
Dim achatCourant As Achat = CType(_achats(iachat), Achat)
' remove from basket
_achats.RemoveAt(iachat)
' decrement the basket total
_totalPanier -= achatCourant.totalAchat
End If
End Sub
Private Function posAchat(ByVal idArticle As Integer) As Integer
' search for a purchase in the purchase list
' returns its position in the list or -1 if not found
Dim achatCourant As Achat
Dim trouvé As Boolean = False
Dim i As Integer = 0
While Not trouvé AndAlso i < _achats.Count
' regular purchase
achatCourant = CType(_achats(i), Achat)
' comparison with article searched
If achatCourant.article.id = idArticle Then
Return i
End If
'next purchase
i += 1
End While
' not found
Return -1
End Function
' identity function
Public Overrides Function ToString() As String
Return _achats.ToString
End Function
End Class
End Namespace
Kommentare:
- Die Klasse [ShoppingCart] verfügt über die folgenden Eigenschaften und Methoden:
die Einkaufsliste des Kunden – eine Liste von Objekten vom Typ [Purchase] | |
fügt einen Kauf zur Liste der Käufe hinzu | |
entfernt den Kauf mit der ID idPurchase | |
Gesamtbetrag der Einkäufe im Warenkorb | |
gibt die Zeichenfolge zurück, die den Warenkorb darstellt |
- Die Methode [posAchat] ist eine Hilfsmethode, die die Position eines durch die Artikelnummer identifizierten Kaufs in der Einkaufsliste zurückgibt. Die Einkaufsliste wird so verwaltet, dass ein mehrfach gekaufter Artikel nur eine Position in der Liste einnimmt. Somit kann ein Kauf anhand der Artikelnummer identifiziert werden. Die Methode [posAchat] gibt -1 zurück, wenn der gesuchte Kauf nicht existiert.
- Die Methode [add] fügt einen neuen Kauf zur Einkaufsliste hinzu. Dabei wird entweder ein neuer Eintrag zur Einkaufsliste hinzugefügt, falls der gekaufte Artikel noch nicht in der Liste vorhanden war, oder die gekaufte Menge erhöht, falls er bereits vorhanden war.
- Mit der Methode [remove] können Sie einen durch eine Nummer identifizierten Einkauf aus der Einkaufsliste entfernen. Wenn der Einkauf nicht existiert, führt die Methode keine Aktion aus.
- Der Gesamtkaufbetrag [totalPanier] wird aktualisiert, wenn Artikel hinzugefügt oder entfernt werden.
1.4.4.4. Die Klasse [PurchaseItems]
Die Schnittstelle [IArticlesDomain] wird durch die folgende Klasse [PurchaseItems] implementiert:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class AchatsArticles
Implements IArticlesDomain
'private fields
Private _articlesDao As IArticlesDao
Private _erreurs As ArrayList
' manufacturer
Public Sub New(ByVal articlesDao As IArticlesDao)
_articlesDao = articlesDao
End Sub
' error list
Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
Get
Return _erreurs
End Get
End Property
' list of items
Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
' list of all items
Try
Return _articlesDao.getAllArticles
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
End Function
' get an item identified by its number
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
' a special item
Try
Return _articlesDao.getArticleById(idArticle)
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
End Function
' buy a basket
Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
' basket purchase - stocks of purchased items must be decremented
_erreurs = New ArrayList
Dim achat As achat
Dim achats As ArrayList = panier.achats
For i As Integer = achats.Count - 1 To 0 Step -1
' decrement stock item i
achat = CType(achats(i), achat)
Try
If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
' we couldn't do the operation
_erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
Else
' the transaction has been completed - the purchase is removed from the basket
panier.enlever(achat.article.id)
End If
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
Next
End Sub
End Class
End Namespace
Kommentare:
- Diese Klasse implementiert die vier Methoden der Schnittstelle [IArticlesDomain]. Sie verfügt über zwei private Felder:
das Datenzugriffsobjekt | |
die Liste möglicher Fehler. Der Zugriff erfolgt über die öffentliche Eigenschaft [errors] |
- Um eine Instanz der Klasse zu erstellen, müssen Sie das Objekt bereitstellen, das den Zugriff auf die Daten ermöglicht:
- Die Methoden [getAllArticles] und [getArticleById] stützen sich auf die gleichnamigen Methoden in der [dao]-Schicht
- Die Methode [buy] validiert den Kauf eines Warenkorbs. Diese Validierung besteht lediglich darin, den Bestand der gekauften Artikel zu verringern. Ein Artikel kann nur gekauft werden, wenn ausreichend Lagerbestand vorhanden ist. Ist dies nicht der Fall, wird der Kauf abgelehnt: Der Artikel verbleibt im Warenkorb und ein Fehler wird in der Liste [errors] gemeldet. Ein validierter Kauf wird aus dem Warenkorb entfernt, und der Bestand des entsprechenden Artikels wird um die gekaufte Menge verringert.
1.4.4.5. Erstellen der [domain]-Layer-Assembly
Das Visual Studio-Projekt ist so konfiguriert, dass die Assembly [webarticles-domain.dll] generiert wird. Diese wird im Ordner [bin] des Projekts generiert:
![]() | ![]() |
1.4.4.6. NUnit-Tests für die [domain]-Schicht
Das Visual Studio-Testprojekt ist wie folgt aufgebaut:

Anmerkungen:
- Das [tests]-Projekt ist vom Typ [Class Library]
- Die [NUnit]-Tests erfordern einen Verweis auf die Assembly [nunit.framework.dll]
- Die [NUnit]-Testklasse ruft über Spring eine Instanz des zu testenden Objekts ab. Daher
- müssen im Ordner [bin] die Spring-Klassendateien
- in [References] einen Verweis auf die Assembly [Spring-Core.dll] aus dem Ordner [bin]
- in [bin] eine Konfigurationsdatei für Spring
- Die Testklasse benötigt die Assembly [webarticles-dao.dll] aus der [dao]-Schicht und die Assembly [webarticles-domain.dll] aus der [domain]-Schicht. Diese wurden im Ordner [bin] abgelegt und ihre Verweise zu den Projektverweisen hinzugefügt.
Eine NUnit-Testklasse für die [domain]-Schicht könnte wie folgt aussehen:
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports istia.st.articles.domain
Imports Spring.Objects.Factory.Xml
Imports System.IO
Namespace istia.st.articles.tests
<TestFixture()> _
Public Class NunitTestArticlesDomain
' the test object
Private articlesDomain As IArticlesDomain
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' retrieve an instance of the Spring object manufacturer
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' request instantiation of the articles dao object
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
' then the articlesdomain aobject
articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
End Sub
<Test()> _
Public Sub getAllArticles()
' visual check
listArticles()
End Sub
<Test()> _
Public Sub getArticleById()
' article deletion
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' research article 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' check
Assert.AreEqual(unArticle.nom, "article3")
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.nom, "article4")
End Sub
<Test()> _
Public Sub acheterPanier()
' article deletion
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' create a basket with two purchases
Dim panier As New panier
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 10))
panier.ajouter(New Achat(New Article(4, "article4", 40, 40, 4), 10))
' checks
Assert.AreEqual(700, panier.totalPanier, 0.000001)
Assert.AreEqual(2, panier.achats.Count)
' shopping cart validation
articlesDomain.acheter(panier)
' checks
Assert.AreEqual(0, articlesDomain.erreurs.Count)
Assert.AreEqual(0, panier.achats.Count)
' research article 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' check
Assert.AreEqual(unArticle.stockactuel, 20)
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.stockactuel, 30)
' new basket
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
' shopping cart validation
articlesDomain.acheter(panier)
' checks
Assert.AreEqual(1, articlesDomain.erreurs.Count)
' research article 3
unArticle = articlesDomain.getArticleById(3)
' check
Assert.AreEqual(unArticle.stockactuel, 20)
End Sub
<Test()> _
Public Sub testRetirerAchats()
' delete contents of ARTICLES
articlesDao.clearAllArticles()
' reads the ARTICLES table
Dim articles As IList = articlesDao.getAllArticles()
Assert.AreEqual(0, articles.Count)
' insertion
Dim article3 As New Article(3, "article3", 30, 30, 3)
articlesDao.ajouteArticle(article3)
Dim article4 As New Article(4, "article4", 40, 40, 4)
articlesDao.ajouteArticle(article4)
' reads the ARTICLES table
articles = articlesDomain.getAllArticles()
Assert.AreEqual(2, articles.Count)
' create a basket with two purchases
Dim monPanier As New Panier
monPanier.ajouter(New Achat(article3, 10))
monPanier.ajouter(New Achat(article4, 10))
' checks
Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
Assert.AreEqual(2, monPanier.achats.Count)
' add a previously purchased item
monPanier.ajouter(New Achat(article3, 10))
' checks
' the total must be increased to 1000
Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
' always 2 items in the basket
Assert.AreEqual(2, monPanier.achats.Count)
' qty item 3 increased to 20
Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
Assert.AreEqual(20, unAchat.qte)
' article 3 is removed from the basket
monPanier.enlever(3)
' checks
' the total must be increased to 400
Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
' 1 item only in basket
Assert.AreEqual(1, monPanier.achats.Count)
' this must be article no. 4
Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
End Sub
' screen listing
Private Sub listArticles()
Dim articles As IList = articlesDomain.getAllArticles
For i As Integer = 0 To articles.Count - 1
Console.WriteLine(CType(articles(i), Article).ToString)
Next
End Sub
End Class
End Namespace
Kommentare:
- Wir wollten ein Testprogramm für die Schnittstelle [IArticlesDomain] schreiben, das unabhängig von ihrer Implementierungsklasse ist. Daher haben wir Spring verwendet, um den Namen der Implementierungsklasse vor dem Testprogramm zu verbergen.
- Die Methode mit dem Attribut <Setup()> ruft aus Spring eine Referenz auf die zu testenden Objekte [articlesdomain] und [articlesdao] ab. Diese sind in der folgenden Datei [spring-config.xml] definiert:
<?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>
Diese Datei gibt an:
- (Fortsetzung)
- für das Singleton [articlesdao] den Namen der Implementierungsklasse [istia.st.articles.dao.ArticlesDaoArrayList] und den Speicherort [webarticles-dao.dll]. Da die Instanziierung keine Parameter erfordert, sind hier keine definiert.
- für das Singleton [articlesdomain] den Namen der Implementierungsklasse [istia.st.articles.domain.AchatsArticles] und den Speicherort [webarticles-domain.dll]. Die Klasse [AchatsArticles] verfügt über einen Konstruktor mit einem Parameter: das Singleton, das den Zugriff auf die [dao]-Schicht verwaltet. Hier ist dies als das zuvor definierte Singleton [articlesdao] definiert.
- Die Testklasse erhält eine Instanz der zu testenden Klasse [articlesdomain] sowie eine Instanz der Datenzugriffsklasse [articlesdao]. Dieser letzte Punkt ist umstritten. Die Testklasse sollte theoretisch keinen Zugriff auf die [dao]-Schicht benötigen, von der sie nicht einmal wissen sollte. Hier haben wir diese „Ethik“ außer Acht gelassen, die, wenn wir sie befolgt hätten, uns dazu gezwungen hätte, neue Methoden in unserer [IArticlesDomain]-Schnittstelle zu erstellen.
Um die [domain]-Schicht zu testen, generieren wir die DLL [tests-webarticles-domain.dll] im Ordner [bin] des Projekts [tests]:
![]() | ![]() |
Anschließend laden wir diese DLL mithilfe der Anwendung [Nunit-Gui] und führen die Tests aus:

Leser, die dieses Dokument auf dem Bildschirm betrachten, werden sehen, dass alle Tests erfolgreich waren. Wir betrachten die [Domain]-Schicht nun als betriebsbereit.
1.4.5. Fazit
Denken Sie daran, dass wir die folgende dreischichtige Webanwendung erstellen wollen:
![]() |
Das M-Modell unserer MVC-Anwendung ist nun geschrieben und getestet. Es wird uns in zwei DLLs [webarticles-dao.dll, webarticles-domain.dll] zur Verfügung gestellt. Wir können nun zur letzten Schicht, der [Web]-Schicht, übergehen, die den C-Controller und die V-Ansichten enthält. Wir werden zunächst eine Methode betrachten, die im Dokument [Web Development with ASP.NET 1.1] vorgestellt wird
- Der C-Controller wird durch zwei Dateien [global.asax, main.aspx] implementiert
- Die V-Ansichten werden von ASPX-Seiten verarbeitet
1.5. Die [Web]-Schicht
Die MVC-Architektur der Webanwendung sieht wie folgt aus:
![]() |
Die Business-Klassen [Domain], die Datenzugriffsklassen [DAO] und die Datenquelle | |
die ASPX-Seiten | |
Alle HTTP-Client-Anfragen durchlaufen die folgenden beiden Controller: global.asax: verarbeitet Ereignisse im Zusammenhang mit dem ersten Start der Anwendung main.aspx: verarbeitet jede Client-Anfrage einzeln |
1.5.1. Die Ansichten
Die Ansichten entsprechen denen, die am Anfang dieses Dokuments vorgestellt wurden:
list.aspx | Die Ansichten befinden sich im Ordner [views] der Anwendung ![]() | |
info.aspx | ||
Warenkorb.aspx | ||
empty-cart.aspx | ||
errors.aspx |
1.5.2. Die Controller
Wie bereits erwähnt, besteht der Controller aus zwei Elementen:
- [global.asax, global.asax.vb]: dient in erster Linie dazu, die Anwendung zu initialisieren und den Kontext für alle Daten einzurichten, die zwischen verschiedenen Clients ausgetauscht werden sollen
- [main.aspx, main.aspx.vb]: der eigentliche Controller, der HTTP-Anfragen von Clients verarbeitet.
Die verschiedenen Client-Anfragen werden an den [main.aspx]-Controller gesendet und enthalten einen Parameter namens [action], der die vom Client angeforderte Aktion angibt:
request | Bedeutung | Controller-Aktion | Mögliche Antworten |
Der Client möchte die Liste der Elemente | - fordert die Liste der Elemente von der Ebene an Beruf | - [LIST] - [FEHLER] | |
Der Client fordert Informationen zu einem der in der Ansicht angezeigten Elemente [LIST] | - fordert das Element aus der Geschäftsschicht an | - [INFO] - [FEHLER] | |
Der Kunde kauft einen Artikel | - fordert den Artikel von der Geschäftsschicht an und legt ihn in den Warenkorb des Kunden | - [INFO] bei Mengenfehler - [LIST] bei keinem Fehler | |
Der Kunde möchte einen Artikel aus seinem Warenkorb entfernen | - den Warenkorb aus der Sitzung abrufen und modifiziere ihn | - [WARENKORB] - [WARENKORB LEEREN] - [FEHLER] | |
Der Kunde möchte seinen Warenkorb | - ruft den Warenkorb aus der Sitzung ab | - [WARENKORB] - [WARENKORB LEER] - [FEHLER] | |
Der Kunde hat seinen Einkauf abgeschlossen und geht zur Kasse | - Aktualisiert die Datenbank mit den Lagerbeständen der gekauften Artikel - Leert den Warenkorb des Kunden von Artikeln , deren Kauf bestätigt wurde | - [LISTE] - [FEHLER] |
1.5.3. Anwendungskonfiguration
Wir werden die Anwendung so konfigurieren, dass sie hinsichtlich Änderungen wie den folgenden so flexibel wie möglich ist:
- Änderungen an den URLs der verschiedenen Ansichten
- Änderungen an den Klassen, die die Schnittstellen [IArticlesDao] und [IArticlesDomain] implementieren
- Änderungen am DBMS, an der Datenbank oder an der Artikeltabelle
1.5.3.1. Änderungen an den URLs
Die Namen der Ansichts-URLs werden zusammen mit einigen anderen Parametern in die Konfigurationsdatei [web.config] der Anwendung aufgenommen:
<?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. Ändern der Klassen, die die Schnittstellen implementieren
Im Sinne der dreischichtigen Architektur müssen die Schichten voneinander isoliert sein. Diese Isolierung wird wie folgt erreicht:
- Schichten kommunizieren über Schnittstellen miteinander, nicht über konkrete Klassen
- Der Code einer Schicht instanziiert niemals selbst die Klasse einer anderen Schicht, um sie zu verwenden. Er fordert lediglich eine Instanz der Schnittstellenimplementierung von einem externen Tool – in diesem Fall Spring – für die Schicht an, die er verwenden möchte. Dazu muss er nicht den Namen der Implementierungsklasse kennen, sondern lediglich den Namen des Spring-Singletons, für das er eine Referenz wünscht.
In unserer Anwendung wird Spring in der [web.config]-Datei der Webanwendung wie folgt konfiguriert:
<?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>
Um auf die [business]-Schicht zuzugreifen, kann eine Klasse in der [web]-Schicht das Singleton [articlesDomain] anfordern. Spring instanziiert dann ein Objekt vom Typ [istia.st.articles.domain.AchatsArticles]. Für diese Instanziierung benötigt es ein Objekt vom Typ [articlesDao], d. h. ein Objekt vom Typ [istia.st.articles.dao.ArticlesDaoArrayList]. Spring instanziiert dann ein solches Objekt. Am Ende des Vorgangs verfügt die [Web]-Schicht, die das [articlesDomain]-Singleton angefordert hat, über die gesamte Kette, die sie mit der Datenquelle verbindet:
![]() |
1.5.3.3. Änderungen in Bezug auf das DBMS oder die Datenbank
Dieser Punkt wird hier ignoriert, da wir in einer Testanwendung ohne DBMS arbeiten. Die Implementierung einer [dao]-Schicht auf Basis eines DBMS werden wir in einem späteren Abschnitt behandeln.
1.5.4. Die <asp:>-Tag-Bibliothek
Betrachten Sie die Ansicht [ERRORS], die eine Liste von Fehlern anzeigt:

Die [ERRORS]-Ansicht ist dafür zuständig, eine Liste von Fehlern anzuzeigen, die der [main.aspx]-Controller unter dem Namen [context.Items("errors")] im Request-Kontext abgelegt hat. Es gibt mehrere Möglichkeiten, eine solche Seite zu schreiben. Hier interessiert uns nur der Teil, der die Fehleranzeige betrifft.
Denken Sie daran, dass eine ASPX-Seite eine HTML-Präsentationsschicht und eine .NET-Code-Schicht hat, die die Daten vorbereitet, die die Präsentationsschicht anzeigen muss. Diese beiden Teile können sich in derselben [aspx]-Datei (WebMatrix-Lösung) oder in zwei Dateien befinden: [aspx] für die Präsentation, [aspx.vb] für den Code. Die letztere Lösung wird von Visual Studio verwendet. Erschwerend kommt hinzu, dass die HTML-Präsentationsschicht auch .NET-Code enthalten kann, was die Trennung zwischen der [Controller]- und der [Präsentations]-Schicht der Ansicht verwischt. Von diesem Ansatz wird generell dringend abgeraten. Das Entfernen des gesamten Codes aus dem [Präsentations]-Bereich erforderte die Erstellung von Tag-Bibliotheken. Diese „verbergen“ den Code unter dem Deckmantel von Tags, die HTML-Tags ähneln. Wir stellen zwei mögliche Lösungen für die [ERRORS]-Seite vor.
Unsere erste Lösung verwendet .NET-Code im [Präsentations]-Abschnitt der Seite. Die ASPX-Seite ruft die Liste der in der Anfrage vorhandenen Fehler in ihrem Controller-Abschnitt [errors.aspx.vb] ab:
Protected erreurs As ArrayList
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
' error recovery
erreurs = CType(context.Items("erreurs"), ArrayList)
End Sub
und zeige sie dann im Abschnitt [presentation, errors.aspx] an:
<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>
Die zweite Lösung verwendet das <asp:repeater>-Tag aus der ASP.NET-Tag-Bibliothek <asp:>. Wenn Sie eine ASPX-Seite grafisch erstellen, steht dieses Tag als Serversteuerelement zur Verfügung, das Sie auf das Entwurfsformular ziehen können. Wenn Sie den ASPX-Code manuell schreiben, können Sie auf die Tag-Bibliothek zurückgreifen.
Mit der <asp:>-Tag-Bibliothek sieht der ASPX-Code für die vorherige [ERRORS]-Ansicht wie folgt aus:
<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>
Die
Das tag-Element dient dazu, eine HTML-Vorlage für die verschiedenen Elemente einer Datenquelle zu wiederholen. Es besteht aus folgenden Elementen:
die HTML-Vorlage, die vor der Anzeige der Datenquellenelemente angezeigt werden soll | |
die HTML-Vorlage, die für jedes Element in der Datenquelle wiederholt wird. Der Ausdruck [<%# Container.DataItem %>] wird verwendet, um den Wert des aktuellen Elements in der Datenquelle anzuzeigen | |
die HTML-Vorlage, die angezeigt werden soll, nachdem die Elemente der Datenquelle angezeigt wurden |
Die Datenquelle ist an das Tag gebunden, typischerweise im [controller]-Bereich der Seite:
Protected WithEvents rptErreurs As System.Web.UI.WebControls.Repeater
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
..
' link errors to rptErreurs
With rptErreurs
.DataSource = context.Items("erreurs")
.DataBind()
End With
End Sub
Diese Bindung kann auch während der Seitenerstellung vorgenommen werden, wenn die Datenquelle bereits bekannt ist, beispielsweise eine vorhandene Datenbank.
In unseren Ansichten verwenden wir ein weiteres Tag: <asp:datagrid>, mit dem wir eine Datenquelle als Tabelle anzeigen können.
1.5.5. Struktur der Visual Studio-Lösung für die Anwendung [webarticles]
Eine Webanwendung ist ein Puzzle mit vielen Teilen. Durch die Verwendung einer MVC-Architektur erhöht sich die Anzahl dieser Teile in der Regel. Die Struktur der [webarticles]-Anwendung in [Visual Studio] sieht wie folgt aus:
![]() | ![]() | ![]() |
![]() |
Kommentare:
- Das [Web]-Projekt ist vom Typ [Class Library] und nicht vom Typ [ASP.NET Web Application], wie man logischerweise erwarten würde. Der Typ [ASP.NET Web Application] setzt das Vorhandensein des IIS-Webservers auf dem Entwicklungsrechner oder auf einem Remote-Rechner voraus. Der IIS-Server ist auf Rechnern mit Windows XP Home Edition standardmäßig nicht enthalten. Viele PCs werden jedoch mit dieser Version verkauft. Damit Leser, die Windows XP verwenden, die behandelte Anwendung implementieren können, werden wir den Cassini-Webserver (siehe Anhang) verwenden, der kostenlos von Microsoft erhältlich ist, und das [ASP.NET-Webanwendung]-Projekt durch ein [Klassenbibliothek]-Projekt ersetzen. Dies bringt einige Nachteile mit sich, die in den Anhängen erläutert werden.
- Die von der Anwendung verwendeten DLLs sind folgende:
enthält die Klassen der Datenzugriffsebene | |
enthält die Klassen der Geschäftsschicht | |
enthält die Spring-Klassen, die es uns ermöglichen, die Web-, Domänen- und DAO-Schichten zu integrieren | |
Logging-Klassen – von Spring verwendet |
Diese DLLs werden im Ordner [bin] abgelegt und den Projektreferenzen hinzugefügt.
1.5.6. Die ASPX-Ansichten
Wie zuvor empfohlen, verwenden wir in unseren ASPX-Ansichten die <asp:>-Tag-Bibliothek.
1.5.6.1. Die Benutzerkomponente [entete.ascx]
Um die Konsistenz zwischen den verschiedenen Ansichten zu gewährleisten, verwenden diese denselben Header, der den Anwendungsnamen zusammen mit dem Menü anzeigt:
![]() | ![]() |
Das Menü ist dynamisch und wird vom Controller festgelegt. Der Controller fügt der an die ASPX-Seite gesendeten Anfrage ein Schlüsselattribut „actions“ hinzu, dessen Wert ein Array aus Hashtable()-Elementen ist. Jedes Element in diesem Array ist ein Dictionary, das dazu dient, eine Option im Kopfmenü zu generieren. Jedes Dictionary hat zwei Schlüssel:
- href: die mit der Menüoption verknüpfte URL
- Link: Der Menütext
Wir werden die Kopfzeile in ein Benutzersteuerelement umwandeln. Ein Benutzersteuerelement kapselt einen Abschnitt einer Seite (Layout und zugehöriger Code) in eine Komponente, die dann auf anderen Seiten wiederverwendet werden kann. Hier möchten wir die Komponente [entete] in anderen Ansichten der Anwendung wiederverwenden. Der Präsentationscode befindet sich in [entete.ascx] und der zugehörige Steuerelementcode in [entete.ascx.vb]. Der Präsentationscode verwendet eine <asp:repeater>-Komponente, um die Tabelle mit den Menüoptionen anzuzeigen:
![]() |
Nr. | Typ | Name | Rolle |
1 | Repeater | rptMenu Datenquelle: ein Array von Wörterbüchern mit zwei Schlüsseln: href, link | Menüoptionen anzeigen |
Der Code für die Darstellung der Seite sieht wie folgt aus:
Kommentare:
- Die Komponente [repeater] ist in den Zeilen 6–14 definiert
- Jedes Element in der mit dem Repeater verknüpften Datenquelle ist ein Dictionary mit zwei Schlüsseln: href (Zeile 9) und link (Zeile 10)
Der zugehörige Steuerungscode lautet wie folgt:
Kommentare:
- Die Komponente [EnteteWebArticles] verfügt über eine öffentliche, schreibgeschützte [actions]-Eigenschaft – Zeile 7
- Diese Eigenschaft ermöglicht es, die <asp:repeater>-Komponente mit dem Namen [rptMenu] – Zeile 10 – an das vom Anwendungscontroller berechnete Optionsarray zu binden – Zeilen 11–12.
Die anderen Ansichten in der Anwendung verwenden die durch [entete.ascx] definierte Kopfzeile. Die Seite [erreurs.aspx] beispielsweise bindet die Kopfzeile mit dem folgenden Code ein:
Kommentare:
- Zeile 1 legt fest, dass das <WA:entete>-Tag mit der durch die Datei [entete.ascx] definierten Komponente verknüpft werden muss. Die Attribute [TagPrefix] und [TagName] sind optional.
- Sobald dies erledigt ist, wird die Komponente mithilfe von Zeile 9 in den Präsentationscode der Seite eingefügt. Zur Laufzeit bindet dieses Tag den Code aus der Seite [entete.ascx] in die ASPX-Seite ein, die es enthält. Der Steuerungscode [erreurs.aspx.vb] übernimmt die Initialisierung dieser Komponente. Dies kann wie folgt erfolgen:
Kommentare:
- Zeile 6 erstellt ein Objekt vom Typ [EnteteWebArticles], dem Typ der zu erstellenden Komponente
- Zeile 11 initialisiert die Eigenschaft [actions] dieses Objekts
1.5.6.2. Die Ansicht [liste.aspx]
1.5.6.2.1. Einleitung
Diese Ansicht zeigt die Liste der zum Verkauf stehenden Artikel an:
![]() | ![]() |
Sie wird nach einer Anfrage an /main?action=list oder /main?action=cartvalidation angezeigt. Die Elemente der Controller-Anfrage lauten wie folgt:
Hashtable()-Objekt – das Array der Menüoptionen | |
ArrayList mit Objekten vom Typ [Item] | |
String-Objekt – Meldung, die am unteren Rand der Seite angezeigt werden soll |
Jeder [Info]-Link in der HTML-Tabelle der Artikel hat eine URL in der Form [?action=info&id=ID], wobei ID das id-Feld des angezeigten Artikels ist.
1.5.6.2.2. Seitenkomponenten
![]() |
Nr. | Typ | Name | Rolle |
Benutzerkomponente | Kopfzeile | Anzeige-Kopfzeile | |
DataGrid | DataGridArticles 3 – zugehörige Spalte: Überschrift: Name, Feld: name 4 – zugehörige Spalte: Überschrift: Preis, Feld: Preis 5 – Hypertext-Spalte: Text: Info, URL-Feld: id, URL-Format: /webarticles/main.aspx?action=info&id={0} | Verkaufsartikel anzeigen | |
Bezeichnung | lblMessage | eine Meldung anzeigen |
Schauen wir uns an, wie man diese Eigenschaften festlegt:
- Wählen Sie in Visual Studio das [DataGrid] aus, um dessen Eigenschaftenfenster aufzurufen:

- Verwenden Sie den Link [AutoFormat] oben, um das Layout des angezeigten Rasters zu verwalten
- und den Link [Property Generator], um dessen Inhalt zu verwalten
1.5.6.2.3. Präsentationscode [liste.aspx]
Kommentare:
- Zeile 9 definiert die Seitenüberschrift
- Die Zeilen 12–24 definieren die Eigenschaften des [DataGrid]
- Zeile 26 definiert das Label [lblMessage]
1.5.6.2.4. Controller-Code [liste.aspx.vb]
Kommentare:
- Die Seitenkomponenten erscheinen in den Zeilen 13–15. Beachten Sie, dass wir ein [EnteteWebArticles]-Objekt mit dem [new]-Operator erstellen mussten, während dies bei den anderen Komponenten nicht erforderlich war. Ohne diese explizite Erstellung trat ein Laufzeitfehler auf, der darauf hinwies, dass das [entete]-Objekt auf nichts verwies. Dieser Punkt verdient eine weitere Untersuchung. Er wurde noch nicht untersucht.
- Die Tabelle der Kopfmenüoptionen wird aus dem Kontext abgerufen, um die [entete]-Komponente der Seite zu initialisieren – Zeile 20
- Die Liste der Artikel wird aus dem Kontext abgerufen – Zeile 22
- um die [DataGridArticles]-Komponente zu initialisieren – Zeilen 24–27
- Die [lblMessage]-Komponente wird mit einer im Kontext platzierten Meldung initialisiert – Zeile 29
1.5.6.3. Die Ansicht [infos.aspx]
1.5.6.3.1. Einführung
Diese Ansicht zeigt Informationen zu einem Artikel an und ermöglicht dessen Kauf:

Sie wird nach einer Anfrage /main?action=infos&id=ID oder einer Anfrage /main?action=achat&id=ID angezeigt, wenn die gekaufte Menge falsch ist. Die Parameter der Controller-Anfrage lauten wie folgt:
Hashtable()-Objekt – das Array der Menüoptionen | |
Objekt vom Typ [Artikel] – anzuzeigender Eintrag | |
String-Objekt – Meldung, die im Falle eines Fehlers bei der Menge angezeigt werden soll | |
String-Objekt – Wert, der im Eingabefeld [Qty] angezeigt werden soll |
Die Felder [msg] und [qte] werden bei einem Eingabefehler bezüglich der Menge verwendet:

Diese Seite enthält ein Formular, das über die Schaltfläche [Kaufen] übermittelt wird. Die Ziel-URL für die POST-Anfrage lautet [?action=purchase&id=ID], wobei ID die ID des gekauften Artikels ist.
1.5.6.3.2. Seitenkomponenten
![]() |
Nr. | Typ | Name | Rolle |
Benutzerkomponente | Kopfzeile | Anzeige-Kopfzeile | |
Literal | litID | Anzeige der Artikelnummer | |
DataGrid | DataGridArtikel 3 – zugehörige Spalte: Überschrift: Name, Feld: name 4 – zugehörige Spalte: Überschrift: Preis, Feld: Preis 5 – zugehörige Spalte: Überschrift: Aktueller Bestand, Feld: currentStock 6 – zugehörige Spalte: Überschrift: Mindestbestand, Feld: stockMinimum | Ein Element anzeigen | |
HTML-Absenden | Formular absenden | ||
HTML-Eingabe runat=server | txtQty | Geben Sie die gekaufte Menge ein | |
label | lblMsgQte | Fehlermeldung |
1.5.6.3.3. Der Präsentationscode [infos.aspx]
Kommentare:
- Der Header ist in der Seite enthalten – Zeile 9
- Das Literal [litId] ist in Zeile 10 definiert
- Das DataGrid [DataGridArticles] ist in den Zeilen 12–34 definiert
- Das Formular ist in den Zeilen 36–46 definiert. Es ist vom Typ POST.
- Das POST-Ziel wird durch eine Variable [strAction] bereitgestellt – Zeile 36. Diese Variable muss vom Controller definiert werden.
- Das Eingabefeld für die gekaufte Menge ist in Zeile 41 definiert. Es handelt sich um eine serverseitige HTML-Komponente (runat=server). Auf der Codeseite wird über ein Objekt darauf zugegriffen.
- Zeile 42 definiert das Label [lblMsgQte], das etwaige Fehlermeldungen bezüglich der eingegebenen Menge enthält
1.5.6.3.4. Der Steuerungscode [infos.aspx.vb]
Kommentare:
- Die Seitenkomponenten sind in den Zeilen 10–14 definiert
- Die Klasse definiert eine öffentliche Eigenschaft [strAction], die zur Festlegung des POST-Ziels des Formulars dient – Zeilen 17–25
- Der anzuzeigende Artikel wird aus dem Anwendungskontext abgerufen – Zeile 30
- Das Array der Kopfmenüoptionen wird aus dem Kontext abgerufen, um die [entete]-Komponente der Seite zu initialisieren – Zeile 32
- Zeilen 33–39: Die Komponente [DataGridArticle] wird an eine Datenquelle vom Typ [ArrayList] gebunden, die nur den in Zeile 30 abgerufenen Artikel enthält
- Die Komponenten [lblMsgQte, txtQte] werden mit Informationen aus dem Kontext initialisiert – Zeilen 42–45
- Die Eigenschaft [straction] wird ebenfalls mit Informationen aus dem Kontext initialisiert – Zeile 47. Diese Variable wird verwendet, um das [action]-Attribut des auf der Seite vorhandenen HTML-Formulars zu generieren:
1.5.6.4. Die Ansicht [panier.aspx]
1.5.6.4.1. Einführung
Diese Ansicht zeigt den Inhalt des Warenkorbs an:

Sie wird als Antwort auf eine Anfrage wie /main?action=cart oder /main?action=cancelpurchase&id=ID angezeigt. Die Anfrageparameter des Controllers lauten wie folgt:
object Hashtable() – das Array der Menüoptionen | |
Objekt vom Typ [Cart] – der anzuzeigende Warenkorb |
Jeder [Entfernen]-Link in der HTML-Tabelle der Warenkorbartikel hat eine URL in der Form [?action=removeitem&id=ID], wobei ID das Feld [id] des Artikels ist, der aus dem Warenkorb entfernt werden soll.
1.5.6.4.2. Seitenkomponenten
![]() |
Nr. | Typ | Name | Rolle |
Benutzerkomponente | Kopfzeile | Anzeige-Kopfzeile | |
DataGrid | DataGridPurchases 3 – zugehörige Spalte – Kopfzeile: Artikel, Feld: Name 4 – zugehörige Spalte – Überschrift: Menge, Feld: Menge 5 – zugehörige Spalte – Überschrift: Preis, Feld: Preis 6 – zugehörige Spalte – Überschrift: Gesamt, Feld: total, Formatierung {0:C} 7 – Hypertext-Spalte – Text: Entfernen, URL: id, URL-Format: /webarticles/main.aspx?action=retirerachat&id={0} | Liste der gekauften Artikel anzeigen | |
Bezeichnung | lblTotal | den fälligen Betrag anzeigen |
1.5.6.4.3. Der Präsentationscode [panier.aspx]
Kommentare
- Zeile 9 enthält die Kopfzeile
- Die Zeilen 12–27 definieren die Komponente [DataGridAchats]
- Zeile 29: Die Komponente [lblTotal] wird definiert
1.5.6.4.4. Der Steuerungscode [panier.aspx.vb]
Kommentare:
- Die Seitenkomponenten werden in den Zeilen 11–13 deklariert
- Die Initialisierung der [header]-Komponente entspricht der in den bereits behandelten Seiten – Zeile 17
- Der anzuzeigende Warenkorb wird aus der Sitzung abgerufen – Zeile 19
- Die Anzeige dieses Warenkorbs mithilfe der Komponente [DataGridAchats] bereitet Probleme. Die Schwierigkeit liegt in der Initialisierung der Komponente. Sehen wir uns ihre Spalten an:
- Die Spalte [Article] ist mit dem Feld [name] in der Datenquelle verknüpft
- Spalte [Qty], die dem Feld [qty] in der Datenquelle zugeordnet ist
- Spalte [Price], die dem Feld [price] in der Datenquelle zugeordnet ist
- Spalte [Total], die dem Feld [total] in der Datenquelle zugeordnet ist
Unsere Datenquelle ist der Warenkorb und die dazugehörige Einkaufsliste. Letztere dient als Datenquelle für das [DataGrid]. Die [Purchase]-Objekte, die die Zeilen des [DataGrid] füllen sollen, verfügen jedoch nicht über die vom [DataGrid] erwarteten Eigenschaften [name, qty, price, total]. Daher erstellen wir hier speziell für das [DataGrid] eine Datenquelle, deren Elemente die vom [DataGrid] erwarteten Eigenschaften aufweisen. Diese Elemente werden vom Typ [PurchaseLine] sein, einer zu diesem Zweck erstellten Klasse, die von der Klasse [Purchase] abgeleitet ist – Zeilen 36–66
- Sobald die Klasse [PurchaseLine] definiert ist, wird die Datenquelle für [DataGridPurchases] aus dem in der Sitzung befindlichen Warenkorb erstellt – Zeilen 20–30
- Der Gesamtkaufbetrag wird mithilfe der Eigenschaft [totalPanier] der Klasse [Panier] angezeigt – Zeile 32
1.5.6.5. Die Ansicht [emptyCart.aspx]
1.5.6.5.1. Einführung
Diese Ansicht zeigt Informationen an, die darauf hinweisen, dass der Warenkorb leer ist:

Sie wird nach einer Anfrage an /main?action=panier oder /main?action=retirerachat&id=ID angezeigt. Die Parameter der Controller-Anfrage lauten wie folgt:
Hashtable()-Objekt – das Array der Menüoptionen |
1.5.6.5.2. Seitenkomponenten
![]() |
Nr. | Typ | Name | Rolle |
Benutzerkomponente | Kopfzeile | Anzeige-Kopfzeile |
1.5.6.5.3. Der Präsentationscode [emptycart.aspx]
Kommentare:
- Der Header ist in Zeile 9 enthalten
1.5.6.5.4. Der Steuerungscode [paniervide.aspx.vb]
Kommentare:
- Wir initialisieren einfach die einzige dynamische Komponente der Seite – Zeile 10
1.5.6.6. Die Ansicht [errors.aspx]
1.5.6.6.1. Einführung
Diese Ansicht wird im Falle von Fehlern angezeigt:

Sie wird nach jeder Anfrage angezeigt, die zu einem Fehler führt, mit Ausnahme der Kaufaktion mit einer falschen Menge, die von der Ansicht [INFOS] behandelt wird. Die Elemente der Controller-Anfrage lauten wie folgt:
Hashtable()-Objekt – das Array der Menüoptionen | |
ArrayList mit [String]-Objekten, die die anzuzeigenden Fehlermeldungen darstellen |
1.5.6.6.2. Seitenelemente
![]() |
Nr. | Typ | Name | Rolle |
Benutzerkomponente | Kopfzeile | Anzeige-Kopfzeile | |
Wiederholer | rptErrors | Fehlerliste anzeigen |
1.5.6.6.3. Der Präsentationscode [errors.aspx]
Kommentare:
- Der Header ist in Zeile 9 definiert
- Die Komponente [rptErrors] ist in den Zeilen 13–19 definiert. Ihr Inhalt stammt aus einer Datenquelle vom Typ [ArrayList], die [String]-Objekte enthält.
1.5.6.6.4. Der Steuerungscode [errors.aspx.vb]
Kommentare:
- Die Komponente [header] wird wie gewohnt initialisiert, Zeilen 9 und 14
- Die [rptErrors]-Komponente wird mit der im Kontext gefundenen [ArrayList]-Fehlerliste initialisiert – Zeilen 16–19
1.5.7. Die Controller global.asax und main.aspx
Wir müssen noch den Kern unserer Webanwendung schreiben: den Controller. Seine Aufgabe ist es:
- die Anfrage des Clients abrufen,
- die vom Client angeforderte Aktion mithilfe der Business-Klassen zu verarbeiten,
- die entsprechende Ansicht als Antwort senden.
1.5.7.1. Der Controller [global.asax.vb]
Wenn die Anwendung ihre allererste Anfrage erhält, wird die Prozedur [Application_Start] in der Datei [global.asax.vb] ausgeführt. Dies geschieht nur einmal. Der Zweck der Prozedur [Application_Start] besteht darin, die von der Webanwendung benötigten Objekte zu initialisieren, die von allen Client-Threads im schreibgeschützten Modus gemeinsam genutzt werden. Diese gemeinsam genutzten Objekte können an zwei Stellen platziert werden:
- den privaten Feldern des Controllers
- im Ausführungskontext der Anwendung (Application)
Die Methode [Application_Start] in der Datei [global.asax.vb] führt die folgenden Aktionen aus:
- Sie überprüft die Datei [web.config] auf die Parameter, die für die ordnungsgemäße Funktion der Anwendung erforderlich sind. Diese wurden in Abschnitt 1.5.3 beschrieben.
- Sie fügt eine Liste aller Fehler im Anwendungskontext in Form eines [ArrayList errors]-Objekts ein. Diese Liste ist leer, wenn keine Fehler vorliegen, existiert aber dennoch.
- Wenn Fehler aufgetreten sind, bricht die Methode [Application_Start] an dieser Stelle ab. Andernfalls fordert sie eine Referenz auf ein Singleton vom Typ [IArticlesDomain] an, bei dem es sich um das Geschäftsobjekt handelt, das der Controller für seine Zwecke verwenden wird. Wie in 1.5.3.2 erläutert, fordert der Controller dieses Singleton vom Spring-Framework an. Dieser Instanziierungsvorgang kann zu verschiedenen Fehlern führen. In diesem Fall werden diese Fehler wiederum im [errors]-Objekt des Anwendungskontexts gespeichert.
Der Controller [global.asax.vb] verfügt über eine [Session_Start]-Prozedur, die bei jedem Eintreffen eines neuen Clients ausgeführt wird. In dieser Prozedur erstellen wir einen leeren Warenkorb für den Client. Dieser Warenkorb wird während der gesamten Dauer der Anfragen dieses bestimmten Clients beibehalten. Der Code könnte wie folgt aussehen:
Kommentare:
- Die erwarteten Parameter in [web.config] sind in einem Array definiert – Zeile 18
- Sie werden in [web.config] gesucht. Sind sie vorhanden, werden sie im Anwendungskontext gespeichert; andernfalls wird ein Fehler in der Fehlerliste [errors] protokolliert – Zeilen 21–33
- Wenn keine Fehler vorliegen, wird Spring um eine Referenz auf das Singleton [articlesDomain] gebeten, das den Zugriff auf die [domain]-Schicht der Anwendung verwaltet – Zeilen 35–47. Etwaige Fehler werden in [errors] protokolliert.
- Fehler werden im Anwendungskontext protokolliert – Zeile 49
- Die Prozedur wird beendet, wenn Fehler aufgetreten sind – Zeile 51
- Wir erstellen ein Array aus drei Dictionaries. Jedes hat zwei Schlüssel: href und link. Dieses Array repräsentiert die drei möglichen Menüoptionen – Zeilen 52–71
- Dieses Array wird im Anwendungskontext gespeichert – Zeile 73
- Für jeden neuen Kunden wird die Prozedur [Session_Start] ausgeführt. In der Sitzung des Kunden wird ein leerer Warenkorb erstellt – Zeilen 78–81
1.5.7.2. Der Controller [main.aspx.vb]
Der Controller [main.aspx.vb] verarbeitet alle Client-Anfragen. Diese Anfragen folgen alle dem Format [/webarticles/main.aspx?action=XX]. Eine Anfrage wird wie folgt verarbeitet:
- Das [errors]-Objekt im Anwendungskontext wird überprüft. Ist es nicht leer, bedeutet dies, dass während der Initialisierung der Anwendung Fehler aufgetreten sind und die Anwendung nicht ausgeführt werden kann. Als Reaktion darauf wird die Ansicht [ERRORS] gesendet.
- Der Parameter [action] der Anfrage wird abgerufen und überprüft. Wenn er keiner bekannten Aktion entspricht, wird die Ansicht [ERRORS] mit einer entsprechenden Fehlermeldung gesendet.
- Ist der Parameter [action] gültig, wird die Anfrage des Clients zur Verarbeitung an eine für die Aktion spezifische Prozedur weitergeleitet:
Methode | Anfrage | Verarbeitung | mögliche Antworten |
GET /main?action=list | - Liste der Elemente aus der Business-Klasse - anzeigen | [LIST] oder [ERRORS] | |
GET /main?action=info&id=ID | - Ruft das Element mit der ID=ID aus der Business-Klasse - es anzeigen | [INFO] oder [ERRORS] | |
POST /main?action=purchase&id=ID - Die gekaufte Menge ist in den übermittelten Parametern enthalten | - Fordere den Artikel mit id=ID von der Business-Klasse - füge ihn im der Kundensitzung | [LIST] oder [INFO] oder [ERRORS] | |
GET /main?action=removePurchase&id=ID | - Entferne den Artikel mit der ID=ID aus der Einkaufsliste in der Kundensitzung | [CART] | |
GET /main?action=cart | - Zeige die Client-Sitzung | [CART] oder [EMPTY_CART] | |
GET /main?action=cartvalidation | - Verringere die Lagerbestände aller Artikel im [LIST] oder [ERRORS] | [LIST] oder [ERRORS] |
Die Controller-Vorlage [main.aspx.vb] könnte wie folgt aussehen:
Kommentare:
- Die Klasse verfügt über zwei private Felder, die von den Methoden gemeinsam genutzt werden – Zeilen 15–16:
- articlesDomain: das Singleton für den Zugriff auf die [domain]-Ebene
- options: das Array von Dictionaries, das Menüoptionen enthält
- Die [Page_Load]-Prozedur:
- initialisiert die beiden privaten Felder der Klasse
- ruft den [action]-Parameter aus der Anfrage ab und führt die Methode aus, die diese Aktion verarbeitet.
1.5.7.3. Die [Page_Load]-Methode
Dieses Ereignis tritt als erstes auf der Seite auf. Der Code lautet wie folgt:
Kommentare:
- Bei jedem Laden der Seite stellen wir sicher, dass die von [global.asax] durchgeführte Anwendungsinitialisierung erfolgreich war.
- Dazu rufen wir die Liste der von [global.asax] dort abgesetzten Fehler aus dem Anwendungskontext ab – Zeile 4
- Ist diese Liste nicht leer, zeigen wir die Ansicht [ERRORS] an – Zeilen 6–10
- Wir rufen das von [global.asax] im Anwendungskontext abgelegte Singleton [articlesDomain] ab und speichern es im privaten Feld [articlesDomain], damit es den verschiedenen Methoden der Klasse zur Verfügung steht – Zeile 12
- Wir führen eine ähnliche Operation mit dem Array der Menüoptionen durch – Zeile 14
- Wir rufen den Parameter [action] aus der Anfrage ab – Zeile 16
- Wir führen die Methode aus, die der angeforderten Aktion entspricht. Eine nicht angegebene Aktion wird als [list]-Aktion behandelt – Zeilen 16–36
1.5.7.4. Behandlung der [list]-Aktion
Dies beinhaltet die Anzeige der Liste der Elemente:

Der Code lautet wie folgt:
Kommentare:
- Alle Fehler werden in einer [ArrayList] gespeichert – Zeile 4
- Die Liste der Elemente wird vom Singleton [articlesDomain] angefordert – Zeilen 5–12
- Wenn Fehler aufgetreten sind, wird die Ansicht [ERRORS] gerendert – Zeilen 13–19
- Andernfalls wird die Ansicht [LIST] zurückgegeben – Zeilen 20–24
1.5.7.5. Behandlung der [info]-Aktion
Der Client hat Informationen zu einem bestimmten Element angefordert:

Der Code lautet wie folgt:
Kommentare:
- Etwaige Fehler werden in einem [ArrayList] gespeichert – Zeile 4
- Die ID des angeforderten Elements wird aus der Anfrage abgerufen – Zeile 6
- Diese ID wird validiert. Sie muss existieren und eine Ganzzahl sein. Ist dies nicht der Fall, wird die Ansicht [ERRORS] mit der entsprechenden Fehlermeldung angezeigt – Zeilen 7–27
- Sobald die ID überprüft wurde, wird das Element vom Singleton [articlesDomain] angefordert. Tritt eine Ausnahme auf, wird die Ansicht [ERRORS] gesendet – Zeilen 29–39
- Wird der Artikel nicht gefunden, wird die Ansicht [ERRORS] gesendet – Zeilen 41–48
- Wird der Artikel gefunden, wird er in die Sitzung des Benutzers aufgenommen und anschließend in der Ansicht [INFO] angezeigt – Zeilen 50–56
1.5.7.6. Verarbeitung der [purchase]-Aktion
Der Kunde hat den in der Ansicht [INFO] angezeigten Artikel gekauft.

Der Code lautet wie folgt:
Kommentare:
- Das in der Sitzung abgelegte Element wird abgerufen – Zeile 5
- Wenn er nicht vorhanden ist (die Sitzung ist möglicherweise abgelaufen), wird die Ansicht [LIST] angezeigt – Zeilen 7–10
- Die gekaufte Menge wird aus der Abfrage abgerufen – Zeile 12
- Ihre Gültigkeit wird überprüft – Zeilen 13–29
- Ist sie ungültig, wird je nach Fall die Ansicht [LIST] angezeigt – Zeile 16 oder die Ansicht [INFO] – Zeilen 24–28
- Wenn alles normal ist, wird der Kauf in den Warenkorb gelegt – Zeilen 31–32
- Anschließend wird die Ansicht [LIST] gesendet – Zeile 34
1.5.7.7. Verarbeitung der [cart]-Aktion
Der Kunde hat mehrere Einkäufe getätigt und möchte den Warenkorb anzeigen:

Der Code lautet wie folgt:
Kommentare:
- Wir rufen den Warenkorb aus der Sitzung ab – Zeile 4. Wir überprüfen hier nicht, ob tatsächlich etwas abgerufen wird. Das sollten wir tun, da die Sitzung möglicherweise abgelaufen ist.
- Wenn der Warenkorb leer ist, senden wir die Ansicht [EMPTY CART] – Zeilen 6–10
- Andernfalls senden wir die Ansicht [SHOPPINGCART] – Zeilen 11–14
1.5.7.8. Verarbeitung der Aktion [removepurchase]
Der Kunde möchte einen Artikel aus seinem Warenkorb entfernen:

Der Code lautet wie folgt:
Kommentare:
- Den Warenkorb aus der Sitzung abrufen – Zeile 4. Wir überprüfen hier nicht, ob tatsächlich etwas abgerufen wurde. Dies sollte jedoch geschehen, da die Sitzung möglicherweise abgelaufen ist.
- Die ID [id] des zu entfernenden Artikels aus der Abfrage abrufen – Zeile 8.
- Der entsprechende Kauf wird aus dem Warenkorb entfernt – Zeile 10
- Die Gültigkeit der ID des gekauften Artikels wurde hier nicht überprüft. Wenn sie einen ungültigen Typ hat, tritt eine Ausnahme auf, die in den Zeilen 11–13 behandelt wird. Wenn sie gültig ist, aber nicht existiert, führt die Methode [cart.remove] – Zeile 10 – nichts aus.
- Der neue Warenkorb wird angezeigt – Zeile 16
1.5.7.9. Verarbeitung der Aktion [validateCart]
Der Kunde möchte seinen Warenkorb bestätigen:

Der Code lautet wie folgt:
Kommentare:
- Wir rufen den Warenkorb aus der Sitzung ab – Zeile 6. Wir überprüfen hier nicht, ob tatsächlich etwas abgerufen wird. Dies sollten wir tun, da die Sitzung möglicherweise abgelaufen ist.
- Wenn die Sitzung abgelaufen ist, haben wir einen [nothing]-Zeiger für den Warenkorb, und die [buy]-Methode – Zeile 9 – löst eine Ausnahme aus, woraufhin die [ERRORS]-Ansicht angezeigt wird. Die Fehlermeldung ist für den Benutzer jedoch unklar.
- Zeilen 8–16: Wir versuchen, den aus der Sitzung abgerufenen Warenkorb zu validieren. Einige Käufe können fehlschlagen, wenn die angeforderte Menge den Lagerbestand des gewünschten Artikels übersteigt. Diese Fälle werden von der [buy]-Methode in einer Fehlerliste gespeichert, die in Zeile 11 abgerufen wird.
- Wenn Fehler vorliegen, wird die Ansicht [ERRORS] gesendet – Zeilen 18–23
- ansonsten wird die Ansicht [LIST] gesendet – Zeilen 25–26
1.6. Fazit
Hier haben wir eine Anwendung unter Verwendung des MVC-Musters entwickelt. Stand April 2005 scheint es keine professionellen MVC-Entwicklungsframeworks für ASP.NET zu geben, die mit denen für Java (Struts, Spring usw.) vergleichbar sind. Es wird erwartet, dass das [Spring.net]-Projekt bald eines veröffentlicht. Bis dahin bietet die oben beschriebene Methode einen praktikablen MVC-Entwicklungsansatz für mittelgroße Anwendungen.































