1. الجزء 1
ملف PDF الخاص بالوثيقة متاح |هنا|.
الأمثلة الواردة في المستند متاحة |هنا|.
1.1. مقدمة
أهداف هذه المقالة:
- كتابة تطبيق ويب ثلاثي الطبقات [واجهة المستخدم، منطق الأعمال، الوصول إلى البيانات]
- تكوين التطبيق باستخدام Spring IOC
- كتابة إصدارات مختلفة عن طريق تغيير تنفيذ طبقة واحدة أو أكثر من الطبقات الثلاث.
الأدوات المستخدمة:
- Visual Studio.NET للتطوير — انظر الملحق، القسم 3.1؛
- خادم الويب Cassini للتنفيذ — انظر الملحق، القسم 3.2؛
- Nunit لاختبار الوحدات — انظر الملحق، القسم 3.4؛
- Spring للتكامل وتكوين طبقات تطبيق الويب — انظر الملحق، القسم 3.3؛
على مقياس المبتدئين والمتوسطين والمتقدمين، يندرج هذا المستند في فئة [المتوسطين والمتقدمين]. يتطلب فهمه عدة متطلبات مسبقة. يمكن العثور على بعضها في المستندات التي قمت بكتابتها. في مثل هذه الحالات، أقوم بالإشارة إليها. وغني عن القول أن هذا مجرد اقتراح وأن للقارئ الحرية في استخدام الموارد التي يفضلها.
- لغة VB.NET: [مقدمة إلى VB.NET من خلال الأمثلة]؛
- البرمجة على الويب في VB.NET: [تطوير الويب باستخدام ASP.NET 1.1]؛
- استخدام جانب IoC في Spring: [Spring IoC لـ .NET]؛
- وثائق Spring.NET: [Spring.NET | الصفحة الرئيسية]
يتبع هذا المستند نفس هيكل المستند المكتوب لـ Java [الهياكل ثلاثية الطبقات وهياكل MVC باستخدام Struts و Spring و Java]. نحن نبني تطبيق ويب MVC ثلاثي الطبقات مكتوب بلغة Java باستخدام VB.NET. النقطة التي نريد توضيحها هنا هي أن منصات التطوير Java و .NET متشابهة بدرجة كافية بحيث يمكن إعادة استخدام المهارات المكتسبة في أحد هذين المجالين في المجال الآخر.
لا يبدو أن هناك حلاً معترفًا به على نطاق واسع لتطوير ASP.NET MVC. يتبنى الحل التالي الطريقة التي تم تقديمها في الوثيقة [تطوير الويب باستخدام ASP.NET 1.1]. ورغم أن هذه الطريقة تتميز باستخدام مفاهيم شائعة في تطوير J2EE، إلا أنه ينبغي النظر إليها على أنها ما هي عليه: إحدى طرق تطوير MVC العديدة. وبمجرد أن تصبح طريقة تطوير MVC في ASP.NET مقبولة على نطاق واسع، ينبغي تبنيها. وقد تكون نسخة .NET من Spring، التي هي قيد التطوير حاليًا، الحل الأول.
1.2. تطبيق webarticles
نقدم هنا مكونات تطبيق ويب مبسط للتجارة الإلكترونية. سيسمح هذا التطبيق لمستخدمي الويب بما يلي:
- عرض قائمة بالعناصر من قاعدة البيانات
- إضافة بعضها إلى عربة التسوق عبر الإنترنت
- تأكيد سلة التسوق. سيؤدي هذا التأكيد ببساطة إلى تحديث مخزون العناصر المشتراة في قاعدة البيانات.
وستكون الشاشات المختلفة التي ستظهر للمستخدم على النحو التالي:
- عرض "القائمة"، الذي يعرض قائمة بالعناصر المعروضة للبيع ![]() | - عرض [INFO]، الذي يوفر معلومات إضافية عن المنتج: ![]() |
- طريقتا العرض [CART] و [EMPTY CART]، اللتان تعرضان محتويات سلة التسوق الخاصة بالعميل
![]() | ![]() |
- عرض [ERRORS]، الذي يبلغ عن أي أخطاء في التطبيق

1.3. البنية العامة للتطبيق
نريد إنشاء تطبيق بالهيكل ثلاثي المستويات التالي:
![]() |
- تتمتع الطبقات الثلاث بالاستقلالية من خلال استخدام الواجهات
- يتم التعامل مع تكامل الطبقات المختلفة بواسطة Spring
- لكل طبقة مساحة أسماء خاصة بها: web (طبقة واجهة المستخدم)، domain (طبقة الأعمال)، و dao (طبقة الوصول إلى البيانات).
سيتبع التطبيق بنية MVC (نموذج-عرض-وحدة تحكم). إذا رجعنا إلى الرسم التخطيطي الطبقي أعلاه، فإن بنية MVC تتناسب معه على النحو التالي:
![]() |
تتم معالجة طلب العميل وفقًا للخطوات التالية:
- يقوم العميل بتوجيه طلب إلى وحدة التحكم. في هذه الحالة، تكون وحدة التحكم عبارة عن صفحة .aspx تؤدي دورًا محددًا. فهي تتولى معالجة جميع طلبات العملاء. وهي نقطة الدخول إلى التطبيق. وهي تمثل الحرف C في نموذج MVC.
- يقوم وحدة التحكم بمعالجة هذا الطلب. وللقيام بذلك، قد تحتاج إلى مساعدة من طبقة الأعمال، المعروفة باسم M في بنية MVC.
- يتلقى وحدة التحكم استجابة من طبقة الأعمال. تمت معالجة طلب العميل. يمكن أن يؤدي ذلك إلى عدة استجابات محتملة. ومن الأمثلة الكلاسيكية على ذلك
- صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
- صفحة تأكيد في الحالات الأخرى
- يختار وحدة التحكم الاستجابة (= العرض) التي سيتم إرسالها إلى العميل. غالبًا ما تكون هذه صفحة تحتوي على عناصر ديناميكية. توفر وحدة التحكم هذه العناصر للعرض.
- يتم إرسال العرض إلى العميل. وهذا هو الحرف V في MVC.
1.4. النموذج
هنا ندرس حرف M في MVC. يتكون النموذج من العناصر التالية:
- فئات الأعمال
- فئات الوصول إلى البيانات
- قاعدة البيانات
1.4.1. قاعدة البيانات
تحتوي قاعدة البيانات على جدول واحد فقط باسم ARTICLES. تم إنشاء هذا الجدول باستخدام أوامر SQL التالية:
CREATE TABLE ARTICLES (
ID INTEGER NOT NULL,
NOM VARCHAR(20) NOT NULL,
PRIX NUMERIC(15,2) NOT NULL,
STOCKACTUEL INTEGER NOT NULL,
STOCKMINIMUM INTEGER NOT NULL
);
/* 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);
المفتاح الأساسي الذي يحدد العنصر بشكل فريد | |
اسم العنصر | |
سعره | |
المخزون الحالي | |
مستوى المخزون الذي يجب عنده إعادة الطلب |
1.4.2. مساحات أسماء النموذج
يتم توفير النموذج M هنا في شكل مساحتين للاسم:
- istia.st.articles.dao: تحتوي على فئات الوصول إلى البيانات في طبقة [dao]
- istia.st.articles.domain: تحتوي على فئات الأعمال الخاصة بطبقة [domain]
سيتم إنشاء كل من مساحات الأسماء هذه داخل ملف "تجميع" خاص بها:
المحتوى | role | |
- [IArticlesDao]: الواجهة المستخدمة للوصول إلى طبقة [dao]. هذه هي الواجهة الوحيدة المرئية لطبقة [domain]. ولا ترى أي واجهات أخرى. - [Article]: فئة تحدد المقالة - [ArticlesDaoArrayList]: فئة تنفيذ واجهة [IArticlesDao] باستخدام [ArrayList] | طبقة الوصول إلى البيانات - تقع بالكامل ضمن طبقة [DAO] في بنية ثلاثية المستويات تطبيق الويب | |
- [IArticlesDomain]: الواجهة للوصول إلى طبقة [domain]. هذه هي الواجهة الوحيدة المرئية لطبقة الويب. ولا ترى أي واجهات أخرى. - [AchatsArticles]: فئة تنفذ [IArticlesDomain] - [Purchase]: فئة تمثل مشتريات العميل - [Cart]: فئة تمثل إجمالي مشتريات العميل | تمثل نموذج مشتريات الويب الويب - موجود بالكامل في طبقة [domain] في البنية ثلاثية المستويات لتطبيق الويب |
1.4.3. طبقة [DAO]
تحتوي طبقة [DAO] على العناصر التالية:
-
[IArticlesDao]: الواجهة للوصول إلى طبقة [dao]
-
[Article]: فئة تحدد المقالة
-
[ArticlesDaoArrayList]: فئة التنفيذ لواجهة [IArticlesDao] باستخدام فئة [ArrayList]
هيكل مشروع [Visual Studio] لطبقة [dao] هو كما يلي:

تعليقات:
- مشروع [dao] هو من نوع [مكتبة الفئات]
- تم وضع الفئات في بنية شجرية تبدأ من المجلد [istia]. وهي جميعها موجودة في مساحة الاسم [istia.st.articles.dao].
1.4.3.1. فئة [Article]
الفئة التي تحدد المقالة هي كما يلي:
Imports System
Namespace istia.st.articles.dao
Public Class Article
' private fields
Private _id As Integer
Private _nom As String
Private _prix As Double
Private _stockactuel As Integer
Private _stockminimum As Integer
' id article
Public Property id() As Integer
Get
Return _id
End Get
Set(ByVal Value As Integer)
If Value <= 0 Then
Throw New Exception("Le champ id [" + Value.ToString + "] est invalide")
End If
Me._id = Value
End Set
End Property
' item name
Public Property nom() As String
Get
Return _nom
End Get
Set(ByVal Value As String)
If Value Is Nothing OrElse Value.Trim.Equals("") Then
Throw New Exception("Le champ nom [" + Value + "] est invalide")
End If
Me._nom = Value
End Set
End Property
' item price
Public Property prix() As Double
Get
Return _prix
End Get
Set(ByVal Value As Double)
If Value < 0 Then
Throw New Exception("Le champ prix [" + Value.ToString + "] est invalide")
End If
Me._prix = Value
End Set
End Property
' current stock item
Public Property stockactuel() As Integer
Get
Return _stockactuel
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Le champ stockActuel [" + Value.ToString + "] est invalide")
End If
Me._stockactuel = Value
End Set
End Property
' minimum stock item
Public Property stockminimum() As Integer
Get
Return _stockminimum
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Le champ stockMinimum [" + Value.ToString + "] est invalide")
End If
Me._stockminimum = Value
End Set
End Property
' default builder
Public Sub New()
End Sub
' builder with properties
Public Sub New(ByVal id As Integer, ByVal nom As String, ByVal prix As Double, ByVal stockactuel As Integer, ByVal stockminimum As Integer)
Me.id = id
Me.nom = nom
Me.prix = prix
Me.stockactuel = stockactuel
Me.stockminimum = stockminimum
End Sub
' article identification method
Public Overrides Function ToString() As String
Return "[" + id.ToString + "," + nom + "," + prix.ToString + "," + stockactuel.ToString + "," + stockminimum.ToString + "]"
End Function
End Class
End Namespace
توفر هذه الفئة:
- منشئًا لتعيين 5 معلومات عن عنصر ما: [id، name، price، currentStock، minimumStock]
- خصائص عامة لقراءة وكتابة المعلومات الخمس.
- التحقق من صحة البيانات التي تم إدخالها للعنصر. إذا كانت البيانات غير صالحة، يتم إصدار استثناء.
- طريقة toString التي تُرجع قيمة عنصر ما كسلسلة نصية. وغالبًا ما يكون ذلك مفيدًا في تصحيح أخطاء التطبيق.
1.4.3.2. واجهة [IArticlesDao]
يتم تعريف واجهة [IArticlesDao] على النحو التالي:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Interface IArticlesDao
' list of all items
Function getAllArticles() As IList
' add an article
Function ajouteArticle(ByVal unArticle As Article) As Integer
' deletes an article
Function supprimeArticle(ByVal idArticle As Integer) As Integer
' modify an article
Function modifieArticle(ByVal unArticle As Article) As Integer
' search for an article
Function getArticleById(ByVal idArticle As Integer) As Article
' deletes all articles
Sub clearAllArticles()
' changes the stock of an item
Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer
End Interface
End Namespace
فيما يلي أدوار الطرق المختلفة في الواجهة:
تُرجع جميع العناصر من مصدر البيانات | |
مسح مصدر البيانات | |
إرجاع كائن [Article] المحدد بواسطة مفتاحه الأساسي | |
تسمح لك بإضافة مقال إلى مصدر البيانات | |
يسمح لك بتعديل مقال في مصدر البيانات | |
يسمح لك بحذف عنصر من مصدر البيانات | |
يسمح لك بتعديل مخزون عنصر في مصدر البيانات |
توفر الواجهة لبرامج العميل عددًا من الطرق التي يتم تعريفها فقط من خلال توقيعاتها. ولا تهتم بالكيفية التي سيتم بها تنفيذ هذه الطرق فعليًا. وهذا يضفي مرونة على التطبيق. يقوم برنامج العميل باستدعاء واجهة بدلاً من استدعاء تنفيذ محدد لتلك الواجهة.
![]() |
سيتم اختيار تطبيق محدد عبر ملف تكوين Spring. لإثبات أن واجهة الوصول إلى البيانات هي المهمة فقط — وليس فئة تطبيقها — لاختبار تطبيق الويب، سنقوم أولاً بتنفيذ مصدر البيانات باستخدام كائن [ArrayList] بسيط. لاحقًا، سنقدم حلاً قائمًا على قاعدة البيانات.
1.4.3.3. فئة التنفيذ [ArticlesDaoArrayList]
يتم تعريف فئة التنفيذ [ArticlesDaoArrayList] على النحو التالي:
Imports System
Imports System.Collections
Namespace istia.st.articles.dao
Public Class ArticlesDaoArrayList
Implements istia.st.articles.dao.IArticlesDao
Private articles As New ArrayList
Private Const nbArticles As Integer = 4
' default builder
Public Sub New()
' we build a few items
For i As Integer = 1 To nbArticles
articles.Add(New Article(i, "article" + i.ToString, i * 10, i * 10, i * 10))
Next
End Sub
' list of all items
Public Function getAllArticles() As IList Implements IArticlesDao.getAllArticles
' returns the list of items
SyncLock Me
Return articles
End SyncLock
End Function
' delete all items
Public Sub clearAllArticles() Implements IArticlesDao.clearAllArticles
' empty the list of items
SyncLock Me
articles.Clear()
End SyncLock
End Sub
' obtain an item identified by its key
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDao.getArticleById
' search for the item in the list
SyncLock Me
Dim ipos As Integer = posArticle(articles, idArticle)
If ipos <> -1 Then
Return CType(articles(ipos), Article)
Else
Return Nothing
End If
End SyncLock
End Function
' add an item to the list of items
Public Function ajouteArticle(ByVal unArticle As Article) As Integer Implements IArticlesDao.ajouteArticle
' add the item to the list of items
SyncLock Me
' we check that it doesn't already exist
Dim ipos As Integer = posArticle(articles, unArticle.id)
If ipos <> -1 Then
Throw New Exception("L'article d'id [" + unArticle.id.ToString + "] existe déjà")
End If
' we add the article
articles.Add(unArticle)
' we return the result
Return 1
End SyncLock
End Function
' modify an article
Public Function modifieArticle(ByVal articleNouveau As Article) As Integer Implements IArticlesDao.modifieArticle
' modify an article
SyncLock Me
' we check that
Dim ipos As Integer = posArticle(articles, articleNouveau.id)
' if it doesn't exist
If ipos = -1 Then Return 0
' it exists - we modify it
articles(ipos) = articleNouveau
' we return the result
Return 1
End SyncLock
End Function
' delete an item identified by its key
Public Function supprimeArticle(ByVal idArticle As Integer) As Integer Implements IArticlesDao.supprimeArticle
' article deletion
SyncLock Me
' we check that
Dim ipos As Integer = posArticle(articles, idArticle)
' if it doesn't exist
If ipos = -1 Then Return 0
' it exists - we remove it
articles.RemoveAt(ipos)
' we return the result
Return 1
End SyncLock
End Function
' change the stock of an item identified by its key
Public Function changerStockArticle(ByVal idArticle As Integer, ByVal mouvement As Integer) As Integer Implements IArticlesDao.changerStockArticle
' change the stock of an item
SyncLock Me
' we check that
Dim ipos As Integer = posArticle(articles, idArticle)
' if it doesn't exist
If ipos = -1 Then Return 0
' it exists - you modify your stock if you can
Dim unArticle As Article = CType(articles(ipos), Article)
' only change stock if it is sufficient
If unArticle.stockactuel + mouvement >= 0 Then
unArticle.stockactuel += mouvement
Return 1
Else
Return 0
End If
End SyncLock
End Function
' search for an item identified by its key
Private Function posArticle(ByVal listArticles As ArrayList, ByVal idArticle As Integer) As Integer
' returns the position of item [idArticle] in the list or -1 if not found
Dim unArticle As Article
For i As Integer = 0 To listArticles.Count - 1
unArticle = CType(listArticles(i), Article)
If unArticle.id = idArticle Then
Return i
End If
Next
' not found
Return -1
End Function
End Class
End Namespace
تعليقات:
- يتم محاكاة مصدر البيانات بواسطة الحقل الخاص [articles] من النوع [ArrayList]
- يقوم منشئ الفئة بإنشاء 4 عناصر في مصدر البيانات بشكل افتراضي.
- تمت مزامنة جميع طرق الوصول إلى البيانات لمنع مشاكل الوصول المتزامن إلى مصدر البيانات. في أي وقت معين، لا يمكن سوى لخيط واحد الوصول إلى طريقة معينة.
- تُرجع طريقة [posArticle] الموضع [0..N] في مصدر البيانات [ArrayList] لمقال محدد برقمه. إذا كان المقال غير موجود، تُرجع الطريقة الموضع -1. تُستخدم هذه الطريقة بشكل متكرر من قبل الطرق الأخرى.
- تضيف طريقة [addArticle] مقالًا إلى قائمة المقالات. وتُرجع عدد المقالات التي تم إدراجها: 1. إذا كان المقال موجودًا بالفعل، يتم إصدار استثناء.
- تسمح لك الطريقة [modifyItem] بتعديل عنصر موجود. وتُرجع عدد العناصر التي تم تعديلها: 1 إذا كان العنصر موجودًا، و0 في حالة عدم وجوده.
- تحذف الطريقة [deleteArticle] مقالًا موجودًا. وتُرجع عدد المقالات المحذوفة: 1 إذا كان المقال موجودًا، و0 في حالة عدم وجوده.
- تُرجع الطريقة [getAllArticles] قائمة بجميع المقالات
- تسترد الطريقة [getArticleById] مقالًا محددًا بواسطة معرّفه. وتُرجع القيمة [nothing] إذا كان المقال غير موجود.
- لا يمثل الكود أي صعوبة حقيقية. نترك للقارئ مراجعته وفهمه.
1.4.3.4. إنشاء تجميع طبقة [dao]
تم تكوين مشروع Visual Studio لإنشاء تجميع [webarticles-dao.dll]. يتم إنشاء هذا التجميع في مجلد [bin] الخاص بالمشروع:
![]() | ![]() |
1.4.3.5. اختبارات NUnit لطبقة [dao]
في Java، يتم اختبار الفئات باستخدام إطار عمل [JUnit]. في .NET، يوفر إطار عمل NUnit نفس إمكانيات اختبار الوحدات:

هيكل مشروع اختبار Visual Studio هو كما يلي:

تعليقات:
- مشروع [tests] هو [مكتبة فئات]
- تتطلب اختبارات [NUnit] مرجعًا إلى تجميع [nunit.framework.dll]
- تسترد فئة اختبار [NUnit] مثيلًا للكائن قيد الاختبار عبر Spring. لذلك،
- في المجلد [bin]، ملفات فئة Spring
- في [References]، مرجع إلى تجميع [Spring-Core.dll] من المجلد [bin]
- في [bin]، ملف تكوين لـ Spring
- تتطلب فئة الاختبار تجميع [webarticles-dao.dll] من طبقة [dao]. وقد تم وضعه في مجلد [bin] وإضافة مرجع له إلى مراجع المشروع.
تتطلب فئة اختبار [NUnit] الوصول إلى الفئات في مساحة اسم [NUnit.Framework]. لذلك، تم تضمين عبارة الاستيراد التالية:
يوجد مساحة الاسم [NUnit.Framework] في التجميع [nunit.framework.dll]، والذي يجب إضافته إلى مراجع المشروع:
![]() | ![]() |
يجب أن يكون التجميع [nunit.framework.dll] موجودًا في القائمة المعروضة إذا كان [NUnit] مثبتًا. ما عليك سوى النقر المزدوج على التجميع لإضافته إلى المشروع:

قد تبدو فئة اختبار [NUnit] لطبقة [DAO] كما يلي:
Imports System
Imports System.Collections
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports System.Threading
Imports Spring.Objects.Factory.Xml
Imports System.IO
Namespace istia.st.articles.tests
<TestFixture()> _
Public Class NunitTestArticlesArrayList
' the test object
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' retrieve an instance of the Spring object manufacturer
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' request instantiation of articlesdao object
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
End Sub
<Test()> _
Public Sub testGetAllArticles()
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testClearAllArticles()
' delete all articles
articlesDao.clearAllArticles()
' all articles are requested
Dim articles As IList = articlesDao.getAllArticles
' verification: there must be 0
Assert.AreEqual(0, articles.Count)
End Sub
<Test()> _
Public Sub testAjouteArticle()
' delete all items
articlesDao.clearAllArticles()
' check: the item table must be empty
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' we add two items
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check: there must be two items
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testSupprimeArticle()
' delete all items
articlesDao.clearAllArticles()
' check: the item table must be empty
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' we add two items
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check: there must be 2 items
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' we delete article 4
articlesDao.supprimeArticle(4)
' check: there must be 1 item left
articles = articlesDao.getAllArticles
Assert.AreEqual(1, articles.Count)
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testModifieArticle()
' delete all items
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' article 3 search
Dim unArticle As Article = articlesDao.getArticleById(3)
' check
Assert.AreEqual(unArticle.nom, "article3")
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.nom, "article4")
' modification article 4
articlesDao.modifieArticle(New Article(4, "article4", 44, 44, 44))
' check
unArticle = articlesDao.getArticleById(4)
Assert.AreEqual(unArticle.prix, 44, 0.000001)
' visual check
listArticles()
End Sub
<Test()> _
Public Sub testGetArticleById()
' article deletion
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDao.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDao.getAllArticles
Assert.AreEqual(2, articles.Count)
' research article 3
Dim unArticle As Article = articlesDao.getArticleById(3)
' check
Assert.AreEqual(unArticle.nom, "article3")
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.nom, "article4")
End Sub
' screen listing
Private Sub listArticles()
Dim articles As IList = articlesDao.getAllArticles
For i As Integer = 0 To articles.Count - 1
Console.WriteLine(CType(articles(i), Article).ToString)
Next
End Sub
<Test()> _
Public Sub testArticleAbsent()
' delete all items
articlesDao.clearAllArticles()
' research article 1
Dim article As article = articlesDao.getArticleById(1)
' check
Assert.IsNull(article)
' modification of a non-existent item
Dim i As Integer = articlesDao.modifieArticle(New article(1, "1", 1, 1, 1))
' had to modify no line
Assert.AreEqual(i, 0)
' deletion of non-existent item
i = articlesDao.supprimeArticle(1)
' had to delete no line
Assert.AreEqual(0, i)
End Sub
<Test()> _
Public Sub testChangerStockArticle()
' delete all items
articlesDao.clearAllArticles()
' add an item
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' add an item
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' creation of 100 threads
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' create thread i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' set the thread name
taches(i).Name = "tache_" & i
' start execution of thread i
taches(i).Start()
Next
' wait for all threads to finish
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' checks - item 3 must have a stock of 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' item 4 stock is decremented
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' check: its stock must not have changed
Assert.AreEqual(0, nbLignes)
' visual check
listArticles()
End Sub
Public Sub décrémente()
' thread launched
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' thread decrements stock
articlesDao.changerStockArticle(3, -1)
' thread terminated
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
End Class
End Namespace
تعليقات:
- أردنا كتابة برنامج اختبار لواجهة [IArticlesDao] يكون مستقلاً عن فئة التنفيذ الخاصة بها. لذلك، استخدمنا Spring لإخفاء اسم فئة التنفيذ عن برنامج الاختبار.
- تسترد طريقة <Setup()> مرجعًا إلى كائن [articlesdao] المراد اختباره من Spring. ويتم تعريف ذلك في ملف [spring-config.xml] التالي:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
<objects>
<object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao"/>
</objects>
يحدد هذا الملف اسم فئة التنفيذ [istia.st.articles.dao.ArticlesDaoArrayList] للواجهة [IArticlesDao] ومكان العثور عليها [webarticles-dao.dll]. وبما أن إنشاء المثيل لا يتطلب أي معلمات، لم يتم تعريف أي منها هنا.
- معظم الاختبارات سهلة الفهم. يُنصح القارئ بقراءة التعليقات.
- تتطلب طريقة [testChangerStockArticle] بعض التوضيح. فهي تنشئ 100 مؤشر ترابط مسؤول عن تخفيض مخزون عنصر معين.
<Test()> _
Public Sub testChangerStockArticle()
' delete all items
articlesDao.clearAllArticles()
' add an item
Dim nbArticles As Integer = articlesDao.ajouteArticle(New Article(3, "article3", 30, 101, 3))
Assert.AreEqual(nbArticles, 1)
' add an item
nbArticles = articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
Assert.AreEqual(nbArticles, 1)
' creation of 100 threads
Dim taches(99) As Thread
For i As Integer = 0 To taches.Length - 1
' create thread i
taches(i) = New Thread(New ThreadStart(AddressOf décrémente))
' set the thread name
taches(i).Name = "tache_" & i
' start execution of thread i
taches(i).Start()
Next
' wait for all threads to finish
For i As Integer = 0 To taches.Length - 1
taches(i).Join()
Next
' checks - item 3 must have a stock of 1
Dim unArticle As Article = articlesDao.getArticleById(3)
Assert.AreEqual(unArticle.nom, "article3")
Assert.AreEqual(1, unArticle.stockactuel)
' item 4 stock is decremented
Dim erreur As Boolean = False
Dim nbLignes As Integer = articlesDao.changerStockArticle(4, -100)
' check: its stock must not have changed
Assert.AreEqual(0, nbLignes)
' visual check
listArticles()
End Sub
هذا لاختبار الوصول المتزامن إلى مصدر البيانات. الطريقة المسؤولة عن تحديث المخزون هي كما يلي:
Public Sub décrémente()
' thread launched
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " lancé")
' thread decrements stock
articlesDao.changerStockArticle(3, -1)
' thread terminated
System.Console.Out.WriteLine(Thread.CurrentThread.Name + " terminé")
End Sub
يقلل مخزون العنصر رقم 3 بمقدار واحد. إذا رجعنا إلى كود الأسلوب [testChangerStockArticle]، نرى أن:
- يتم تهيئة مخزون العنصر رقم 3 إلى 101
- ستقوم كل خيط من الخيوط المائة بتخفيض هذا المخزون بمقدار واحد
- وبالتالي، يجب أن يكون لدينا مخزون قدره 1 عند انتهاء تنفيذ جميع الخيوط
علاوة على ذلك، وفي إطار هذه الطريقة نفسها، نحاول تعيين مخزون العنصر رقم 4 على قيمة سالبة. ومن المفترض أن تفشل هذه المحاولة.
لاختبار طبقة [dao]، نقوم بإنشاء ملف DLL [tests-webarticles-dao.dll] في مجلد [bin] التابع لمشروع [tests]:
![]() | ![]() |
ثم، باستخدام تطبيق [Nunit-Gui]، نقوم بتحميل ملف DLL هذا وتشغيل الاختبارات:

في النافذة اليسرى، نرى قائمة بالطرق التي تم اختبارها. يشير لون النقطة التي تسبق اسم كل طريقة إلى ما إذا كانت الطريقة قد نجحت (أخضر) أو فشلت (أحمر). سيلاحظ القراء الذين يقرؤون هذا المستند على الشاشة أن جميع الاختبارات قد نجحت. وبالتالي، سنعتبر أن طبقة [dao] تعمل بشكل سليم.
1.4.4. طبقة [domain]
تحتوي طبقة [domain] على العناصر التالية:
-
[IArticlesDomain]: الواجهة للوصول إلى طبقة [domain]
-
[Purchase]: فئة تحدد عملية الشراء
-
[ShoppingCart]: فئة تحدد عربة التسوق
-
[ProductPurchases]: الفئة التي تنفذ واجهة [IArticlesDomain]
هيكل حل [Visual Studio] لطبقة [domain] هو كما يلي:

تعليقات:
- مشروع [domain] هو من نوع [مكتبة الفئات]
- تم وضع الفئات في بنية شجرية تبدأ من المجلد [istia]. وهي جميعها موجودة في مساحة الاسم [istia.st.articles.domain].
- تم وضع ملف DLL الخاص بطبقة [dao] في مجلد [bin] الخاص بالمشروع الجديد. بالإضافة إلى ذلك، تمت إضافة ملف DLL هذا كمرجع للمشروع.
1.4.4.1. واجهة [IArticlesDomain]
تفصل واجهة [IArticlesDomain] طبقة [business] عن طبقة [web]. وتصل الطبقة الأخيرة إلى طبقة [business/domain] عبر هذه الواجهة دون الاهتمام بالفئة التي تنفذها فعليًا. تحدد الواجهة الإجراءات التالية للوصول إلى طبقة الأعمال:
Imports Article = istia.st.articles.dao.Article
Namespace istia.st.articles.domain
Public Interface IArticlesDomain
' methods
Sub acheter(ByVal panier As Panier)
Function getAllArticles() As IList
Function getArticleById(ByVal idArticle As Integer) As Article
ReadOnly Property erreurs() As ArrayList
End Interface
End Namespace
تُرجع قائمة كائنات [Article] من مصدر البيانات المرتبط | |
تُرجع كائن [Article] المحدد بواسطة [idArticle] | |
يعالج سلة التسوق الخاصة بالعميل عن طريق خصم كمية المشتريات من المخزون - قد يفشل إذا كان المخزون غير كافٍ | |
تُرجع قائمة بالأخطاء التي حدثت - تكون فارغة في حالة عدم وجود أخطاء |
1.4.4.2. فئة [Purchase]
تمثل فئة [Purchase] عملية شراء قام بها العميل:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class Achat
' private fields
Private _article As article
Private _qte As Integer
' default builder
Public Sub New()
End Sub
' builder with parameters
Public Sub New(ByVal unArticle As article, ByVal qte As Integer)
' we go through the properties
Me.article = unArticle
Me.qte = qte
End Sub
' item purchased
Public Property article() As article
Get
Return _article
End Get
Set(ByVal Value As article)
_article = Value
End Set
End Property
' qty purchased
Public Property qte() As Integer
Get
Return _qte
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New Exception("Quantité [" + Value.ToString + "] invalide")
End If
_qte = Value
End Set
End Property
' total purchase
Public ReadOnly Property totalAchat() As Double
Get
Return _qte * _article.prix
End Get
End Property
' identity
Public Overrides Function ToString() As String
Return "[" + _article.ToString + "," + _qte.ToString + "]"
End Function
End Class
End Namespace
تعليقات:
- تحتوي فئة [Purchase] على الخصائص والطرق التالية:
العنصر الذي تم شراؤه | |
الكمية المشتراة | |
مبلغ الشراء | |
تمثيل الكائن كسلسلة |
- يحتوي على منشئ يقوم بتهيئة خصائص [item, qty] التي تحدد عملية الشراء.
1.4.4.3. فئة [Cart]
تمثل فئة [Cart] إجمالي مشتريات العميل:
Namespace istia.st.articles.domain
Public Class Panier
' private fields
Private _achats As New ArrayList
Private _totalPanier As Double = 0
' default builder
Public Sub New()
End Sub
' list of purchases
Public ReadOnly Property achats() As ArrayList
Get
Return _achats
End Get
End Property
' total purchases
Public ReadOnly Property totalPanier() As Double
Get
Return _totalPanier
End Get
End Property
' methods
Public Sub ajouter(ByVal unAchat As Achat)
' find out if the purchase already exists
Dim iAchat As Integer = posAchat(unAchat.article.id)
If iAchat <> -1 Then
' we found
Dim achatCourant As Achat = CType(_achats(iAchat), Achat)
achatCourant.qte += unAchat.qte
Else
' we didn't find
_achats.Add(unAchat)
End If
' increment the basket total
_totalPanier += unAchat.totalAchat
End Sub
' remove a purchase
Public Sub enlever(ByVal idAchat As Integer)
' we're looking to buy
Dim iachat As Integer = posAchat(idAchat)
' if found, remove
If iachat <> -1 Then
Dim achatCourant As Achat = CType(_achats(iachat), Achat)
' remove from basket
_achats.RemoveAt(iachat)
' decrement the basket total
_totalPanier -= achatCourant.totalAchat
End If
End Sub
Private Function posAchat(ByVal idArticle As Integer) As Integer
' search for a purchase in the purchase list
' returns its position in the list or -1 if not found
Dim achatCourant As Achat
Dim trouvé As Boolean = False
Dim i As Integer = 0
While Not trouvé AndAlso i < _achats.Count
' regular purchase
achatCourant = CType(_achats(i), Achat)
' comparison with article searched
If achatCourant.article.id = idArticle Then
Return i
End If
'next purchase
i += 1
End While
' not found
Return -1
End Function
' identity function
Public Overrides Function ToString() As String
Return _achats.ToString
End Function
End Class
End Namespace
تعليقات:
- تحتوي فئة [ShoppingCart] على الخصائص والطرق التالية:
قائمة التسوق الخاصة بالعميل - قائمة من الكائنات من النوع [Purchase] | |
تضيف عملية شراء إلى قائمة عمليات الشراء | |
يزيل عملية الشراء التي تحمل معرف idPurchase | |
المبلغ الإجمالي للمشتريات في سلة التسوق | |
تُرجع السلسلة التي تمثل سلة التسوق |
- طريقة [posAchat] هي طريقة مساعدة تُرجع الموضع في قائمة المشتريات لمشتريات محددة برقم العنصر. تُدار قائمة التسوق بحيث لا يشغل العنصر الذي تم شراؤه عدة مرات سوى موضع واحد في القائمة. وبالتالي، يمكن تحديد المشتريات برقم العنصر. تُرجع طريقة [posAchat] القيمة -1 إذا كانت المشتريات التي يتم البحث عنها غير موجودة.
- تضيف طريقة [add] عملية شراء جديدة إلى قائمة المشتريات. وهذا إما يضيف إدخالًا جديدًا إلى قائمة المشتريات إذا لم يكن العنصر الذي تم شراؤه موجودًا بالفعل في القائمة، أو يزيد الكمية المشتراة إذا كان موجودًا بالفعل.
- تسمح لك طريقة [remove] بإزالة عملية شراء محددة برقم من قائمة عمليات الشراء. إذا لم تكن عملية الشراء موجودة، فلن تقوم الطريقة بأي شيء.
- يتم تحديث المبلغ الإجمالي للمشتريات [totalPanier] مع إضافة العناصر وإزالتها.
1.4.4.4. فئة [PurchaseItems]
سيتم تنفيذ واجهة [IArticlesDomain] بواسطة فئة [PurchaseItems] التالية:
Imports istia.st.articles.dao
Namespace istia.st.articles.domain
Public Class AchatsArticles
Implements IArticlesDomain
'private fields
Private _articlesDao As IArticlesDao
Private _erreurs As ArrayList
' manufacturer
Public Sub New(ByVal articlesDao As IArticlesDao)
_articlesDao = articlesDao
End Sub
' error list
Public ReadOnly Property erreurs() As ArrayList Implements IArticlesDomain.erreurs
Get
Return _erreurs
End Get
End Property
' list of items
Public Function getAllArticles() As IList Implements IArticlesDomain.getAllArticles
' list of all items
Try
Return _articlesDao.getAllArticles
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
End Function
' get an item identified by its number
Public Function getArticleById(ByVal idArticle As Integer) As Article Implements IArticlesDomain.getArticleById
' a special item
Try
Return _articlesDao.getArticleById(idArticle)
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
End Function
' buy a basket
Public Sub acheter(ByVal panier As Panier) Implements IArticlesDomain.acheter
' basket purchase - stocks of purchased items must be decremented
_erreurs = New ArrayList
Dim achat As achat
Dim achats As ArrayList = panier.achats
For i As Integer = achats.Count - 1 To 0 Step -1
' decrement stock item i
achat = CType(achats(i), achat)
Try
If _articlesDao.changerStockArticle(achat.article.id, -achat.qte) = 0 Then
' we couldn't do the operation
_erreurs.Add("L'achat " + achat.ToString + " n'a pu se faire - Vérifiez les stocks")
Else
' the transaction has been completed - the purchase is removed from the basket
panier.enlever(achat.article.id)
End If
Catch ex As Exception
_erreurs = New ArrayList
_erreurs.Add("Erreur d'accès aux données : " + ex.Message)
End Try
Next
End Sub
End Class
End Namespace
تعليقات:
- تنفذ هذه الفئة الطرق الأربع لواجهة [IArticlesDomain]. وتحتوي على حقلين خاصين:
كائن الوصول إلى البيانات | |
قائمة الأخطاء المحتملة. يمكن الوصول إليها عبر الخاصية العامة [errors] |
- لإنشاء مثيل للفئة، يجب توفير الكائن الذي يسمح بالوصول إلى البيانات:
- تعتمد طريقتا [getAllArticles] و [getArticleById] على الطرق التي تحمل نفس الاسم في طبقة [dao]
- تقوم طريقة [buy] بالتحقق من صحة عملية شراء سلة التسوق. يتضمن هذا التحقق ببساطة خصم كمية العناصر المشتراة من المخزون. لا يمكن شراء عنصر ما إلا إذا كان هناك مخزون كافٍ منه. إذا لم يكن الأمر كذلك، يتم رفض عملية الشراء: يبقى العنصر في سلة التسوق ويتم الإبلاغ عن خطأ في قائمة [errors]. تتم إزالة عملية الشراء التي تم التحقق من صحتها من السلة، ويتم خصم الكمية المشتراة من مخزون العنصر المقابل.
1.4.4.5. إنشاء تجميع طبقة [domain]
تم تكوين مشروع Visual Studio لإنشاء تجميع [webarticles-domain.dll]. يتم إنشاء هذا التجميع في مجلد [bin] الخاص بالمشروع:
![]() | ![]() |
1.4.4.6. اختبارات NUnit لطبقة [domain]
هيكل مشروع اختبار Visual Studio هو كما يلي:

تعليقات:
- مشروع [tests] هو من نوع [مكتبة الفئات]
- تتطلب اختبارات [NUnit] مرجعًا إلى تجميع [nunit.framework.dll]
- تسترد فئة اختبار [NUnit] مثيلًا للكائن قيد الاختبار عبر Spring. لذلك،
- في المجلد [bin]، ملفات فئة Spring
- في [References]، مرجع إلى تجميع [Spring-Core.dll] من المجلد [bin]
- في [bin]، ملف تكوين لـ Spring
- تتطلب فئة الاختبار تجميع [webarticles-dao.dll] من طبقة [dao] وتجميع [webarticles-domain.dll] من طبقة [domain]. وقد تم وضع هذين التجميعين في مجلد [bin] وإضافة إشاراتهما إلى مراجع المشروع.
قد تبدو فئة اختبار NUnit لطبقة [domain] كما يلي:
Imports NUnit.Framework
Imports istia.st.articles.dao
Imports istia.st.articles.domain
Imports Spring.Objects.Factory.Xml
Imports System.IO
Namespace istia.st.articles.tests
<TestFixture()> _
Public Class NunitTestArticlesDomain
' the test object
Private articlesDomain As IArticlesDomain
Private articlesDao As IArticlesDao
<SetUp()> _
Public Sub init()
' retrieve an instance of the Spring object manufacturer
Dim factory As XmlObjectFactory = New XmlObjectFactory(New FileStream("spring-config.xml", FileMode.Open))
' request instantiation of the articles dao object
articlesDao = CType(factory.GetObject("articlesdao"), IArticlesDao)
' then the articlesdomain aobject
articlesDomain = CType(factory.GetObject("articlesdomain"), IArticlesDomain)
End Sub
<Test()> _
Public Sub getAllArticles()
' visual check
listArticles()
End Sub
<Test()> _
Public Sub getArticleById()
' article deletion
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' research article 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' check
Assert.AreEqual(unArticle.nom, "article3")
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.nom, "article4")
End Sub
<Test()> _
Public Sub acheterPanier()
' article deletion
articlesDao.clearAllArticles()
' check
Dim articles As IList = articlesDomain.getAllArticles
Assert.AreEqual(0, articles.Count)
' 2 items added
articlesDao.ajouteArticle(New Article(3, "article3", 30, 30, 3))
articlesDao.ajouteArticle(New Article(4, "article4", 40, 40, 4))
' check
articles = articlesDomain.getAllArticles
Assert.AreEqual(2, articles.Count)
' create a basket with two purchases
Dim panier As New panier
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 10))
panier.ajouter(New Achat(New Article(4, "article4", 40, 40, 4), 10))
' checks
Assert.AreEqual(700, panier.totalPanier, 0.000001)
Assert.AreEqual(2, panier.achats.Count)
' shopping cart validation
articlesDomain.acheter(panier)
' checks
Assert.AreEqual(0, articlesDomain.erreurs.Count)
Assert.AreEqual(0, panier.achats.Count)
' research article 3
Dim unArticle As Article = articlesDomain.getArticleById(3)
' check
Assert.AreEqual(unArticle.stockactuel, 20)
' research article 4
unArticle = articlesDao.getArticleById(4)
' check
Assert.AreEqual(unArticle.stockactuel, 30)
' new basket
panier.ajouter(New Achat(New Article(3, "article3", 30, 30, 3), 100))
' shopping cart validation
articlesDomain.acheter(panier)
' checks
Assert.AreEqual(1, articlesDomain.erreurs.Count)
' research article 3
unArticle = articlesDomain.getArticleById(3)
' check
Assert.AreEqual(unArticle.stockactuel, 20)
End Sub
<Test()> _
Public Sub testRetirerAchats()
' delete contents of ARTICLES
articlesDao.clearAllArticles()
' reads the ARTICLES table
Dim articles As IList = articlesDao.getAllArticles()
Assert.AreEqual(0, articles.Count)
' insertion
Dim article3 As New Article(3, "article3", 30, 30, 3)
articlesDao.ajouteArticle(article3)
Dim article4 As New Article(4, "article4", 40, 40, 4)
articlesDao.ajouteArticle(article4)
' reads the ARTICLES table
articles = articlesDomain.getAllArticles()
Assert.AreEqual(2, articles.Count)
' create a basket with two purchases
Dim monPanier As New Panier
monPanier.ajouter(New Achat(article3, 10))
monPanier.ajouter(New Achat(article4, 10))
' checks
Assert.AreEqual(700.0, monPanier.totalPanier, 0.000001)
Assert.AreEqual(2, monPanier.achats.Count)
' add a previously purchased item
monPanier.ajouter(New Achat(article3, 10))
' checks
' the total must be increased to 1000
Assert.AreEqual(1000.0, monPanier.totalPanier, 0.000001)
' always 2 items in the basket
Assert.AreEqual(2, monPanier.achats.Count)
' qty item 3 increased to 20
Dim unAchat As Achat = CType(monPanier.achats(0), Achat)
Assert.AreEqual(20, unAchat.qte)
' article 3 is removed from the basket
monPanier.enlever(3)
' checks
' the total must be increased to 400
Assert.AreEqual(400.0, monPanier.totalPanier, 0.000001)
' 1 item only in basket
Assert.AreEqual(1, monPanier.achats.Count)
' this must be article no. 4
Assert.AreEqual(4, CType(monPanier.achats(0), Achat).article.id)
End Sub
' screen listing
Private Sub listArticles()
Dim articles As IList = articlesDomain.getAllArticles
For i As Integer = 0 To articles.Count - 1
Console.WriteLine(CType(articles(i), Article).ToString)
Next
End Sub
End Class
End Namespace
تعليقات:
- أردنا كتابة برنامج اختبار لواجهة [IArticlesDomain] يكون مستقلاً عن فئة التنفيذ الخاصة بها. لذلك، استخدمنا Spring لإخفاء اسم فئة التنفيذ عن برنامج الاختبار.
- تسترد طريقة السمة <Setup()> مرجعًا إلى كائني [articlesdomain] و[articlesdao] المراد اختبارهما من Spring. وهما معرّفان في ملف [spring-config.xml] التالي:
<?xml version="1.0" encoding="iso-8859-1" ?>
<!DOCTYPE objects PUBLIC "-//SPRING//DTD OBJECT//EN"
"http://www.springframework.net/dtd/spring-objects.dtd">
<objects>
<object id="articlesdao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao" />
<object id="articlesdomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
<constructor-arg index="0">
<ref object="articlesdao" />
</constructor-arg>
</object>
</objects>
يشير هذا الملف إلى:
- (تابع)
- بالنسبة للسينجلتون [articlesdao]، اسم فئة التنفيذ [istia.st.articles.dao.ArticlesDaoArrayList] ومكان العثور عليها [webarticles-dao.dll]. نظرًا لأن إنشاء المثيل لا يتطلب أي معلمات، لم يتم تعريف أي منها هنا.
- بالنسبة للسينجلتون [articlesdomain]، اسم فئة التنفيذ [istia.st.articles.domain.AchatsArticles] ومكان العثور عليها [webarticles-domain.dll]. تحتوي الفئة [AchatsArticles] على منشئ بمعلمة واحدة: السينجلتون الذي يدير الوصول إلى طبقة [dao]. هنا، يتم تعريف هذا على أنه السينجلتون [articlesdao] المحدد سابقًا.
- تحصل فئة الاختبار على مثيل للفئة قيد الاختبار [articlesdomain] بالإضافة إلى مثيل لفئة الوصول إلى البيانات [articlesdao]. هذه النقطة الأخيرة مثيرة للجدل. من الناحية النظرية، لا ينبغي أن تحتاج فئة الاختبار إلى الوصول إلى طبقة [dao]، التي لا يُفترض حتى أن تكون على علم بها. هنا، تجاهلنا هذه "الأخلاقيات"، التي لو اتبعناها، لكان علينا إنشاء طرق جديدة في واجهة [IArticlesDomain] الخاصة بنا.
لاختبار طبقة [domain]، نقوم بإنشاء ملف DLL [tests-webarticles-domain.dll] في مجلد [bin] التابع لمشروع [tests]:
![]() | ![]() |
ثم، باستخدام تطبيق [Nunit-Gui]، نقوم بتحميل ملف DLL هذا وتشغيل الاختبارات:

سيلاحظ القراء الذين يشاهدون هذا المستند على الشاشة أن جميع الاختبارات كانت ناجحة. سنعتبر الآن أن طبقة [domain] أصبحت جاهزة للعمل.
1.4.5. الخلاصة
تذكر أننا نريد إنشاء تطبيق الويب ثلاثي الطبقات التالي:
![]() |
تم الآن كتابة واختبار نموذج M لتطبيق MVC الخاص بنا. ويتم توفيره لنا في ملفين DLL [webarticles-dao.dll، webarticles-domain.dll]. يمكننا الانتقال إلى الطبقة الأخيرة، وهي طبقة [web]، التي تحتوي على وحدة التحكم C وطرق العرض V. سننظر أولاً في طريقة مقدمة في الوثيقة [تطوير الويب باستخدام ASP.NET 1.1]
- يتم تنفيذ وحدة التحكم C بواسطة ملفين [global.asax، main.aspx]
- يتم التعامل مع طرق العرض V بواسطة صفحات aspx
1.5. طبقة [web]
ستكون بنية MVC لتطبيق الويب كما يلي:
![]() |
فئات الأعمال [domain]، وفئات الوصول إلى البيانات [DAO]، ومصدر البيانات | |
صفحات ASPX | |
تمر جميع طلبات عملاء HTTP عبر وحدتي التحكم التاليتين: global.asax: تتعامل مع الأحداث المتعلقة بالتشغيل الأولي للتطبيق main.aspx: تعالج طلبات كل عميل على حدة |
1.5.1. طرق العرض
تتوافق طرق العرض مع تلك المعروضة في بداية هذا المستند:
list.aspx | توجد طرق العرض في مجلد [views] الخاص بالتطبيق ![]() | |
info.aspx | ||
cart.aspx | ||
empty-cart.aspx | ||
errors.aspx |
1.5.2. وحدات التحكم
كما ذكرنا سابقًا، تتكون وحدة التحكم من عنصرين:
- [global.asax، global.asax.vb]: تُستخدم بشكل أساسي لتهيئة التطبيق وإعداد السياق لجميع البيانات التي سيتم مشاركتها بين العملاء المختلفين
- [main.aspx، main.aspx.vb]: وحدة التحكم الفعلية، التي تتولى معالجة طلبات HTTP الواردة من العملاء.
سيتم إرسال طلبات العملاء المختلفة إلى وحدة التحكم [main.aspx] وستحتوي على معلمة تسمى [action] تحدد الإجراء المطلوب من قبل العميل:
request | المعنى | إجراء وحدة التحكم | الاستجابات المحتملة |
يريد العميل قائمة العناصر | - يطلب قائمة العناصر من الطبقة المهنة | - [LIST] - [أخطاء] | |
يطلب العميل معلومات عن أحد العناصر المعروضة في العرض [LIST] | - يطلب العنصر من طبقة الأعمال | - [INFO] - [ERRORS] | |
يقوم العميل بشراء عنصر | - يطلب المنتج من طبقة الأعمال ويضيفه إلى سلة التسوق الخاصة به | - [معلومات] في حالة وجود خطأ في الكمية - [LIST] في حالة عدم وجود خطأ | |
يريد العميل إزالة عنصر من سلة التسوق | - استرجاع سلة التسوق من الجلسة وتعديلها | - [سلة التسوق] - [إفراغ سلة التسوق] - [أخطاء] | |
يريد العميل عرض عربة التسوق | - استرداد سلة التسوق من الجلسة | - [عربة التسوق] - [سلة التسوق فارغة] - [أخطاء] | |
لقد انتهى العميل من التسوق ويشرع في إتمام عملية الدفع | - تحديث قاعدة البيانات بمستويات المخزون المنتجات التي تم شراؤها - يفرغ سلة التسوق الخاصة بالعميل من العناصر التي تم تأكيد شرائها | - [قائمة] - [أخطاء] |
1.5.3. تكوين التطبيق
سنسعى إلى تكوين التطبيق لجعله مرنًا قدر الإمكان فيما يتعلق بالتغييرات مثل:
- التغييرات في عناوين URL لمختلف طرق العرض
- التغييرات في الفئات التي تنفذ واجهات [IArticlesDao] و [IArticlesDomain]
- التغييرات في نظام إدارة قواعد البيانات (DBMS) أو قاعدة البيانات أو جدول المقالات
1.5.3.1. تغييرات عناوين URL
سيتم وضع أسماء عناوين URL للعروض في ملف تكوين [web.config] الخاص بالتطبيق جنبًا إلى جنب مع بعض المعلمات الأخرى:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
..
<appSettings>
<add key="urlMain" value="/webarticles/main.aspx"/>
<add key="urlInfos" value="vues/infos.aspx"/>
<add key="urlErreurs" value="vues/erreurs.aspx"/>
<add key="urlListe" value="vues/liste.aspx"/>
<add key="urlPanier" value="vues/panier.aspx"/>
<add key="urlPanierVide" value="vues/paniervide.aspx"/>
</appSettings>
</configuration>
1.5.3.2. تغيير الفئات التي تنفذ الواجهات
انطلاقاً من مبدأ البنى ثلاثية المستويات، يجب عزل الطبقات عن بعضها البعض. ويتم تحقيق هذا العزل على النحو التالي:
- تتواصل الطبقات مع بعضها البعض عبر واجهات، وليس عبر فئات محددة
- لا يقوم كود طبقة ما أبدًا بإنشاء مثيل لفئة طبقة أخرى من أجل استخدامها. بل يطلب ببساطة مثيلًا لتنفيذ الواجهة من أداة خارجية — في هذه الحالة، Spring — للطبقة التي يرغب في استخدامها. للقيام بذلك، نعلم أنه لا يحتاج إلى معرفة اسم فئة التنفيذ، بل فقط اسم Spring singleton الذي يريد مرجعًا له.
في تطبيقنا، سيتم تكوين Spring في ملف [web.config] الخاص بتطبيق الويب على النحو التالي:
<?xml version="1.0" encoding="iso-8859-1" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context type="Spring.Context.Support.XmlApplicationContext, Spring.Core">
<resource uri="config://spring/objects" />
</context>
<objects>
<object id="articlesDao" type="istia.st.articles.dao.ArticlesDaoArrayList, webarticles-dao" />
<object id="articlesDomain" type="istia.st.articles.domain.AchatsArticles, webarticles-domain">
<constructor-arg index="0">
<ref object="articlesDao" />
</constructor-arg>
</object>
</objects>
</spring>
<appSettings>
<add key="urlMain" value="/webarticles/main.aspx"/>
<add key="urlInfos" value="vues/infos.aspx"/>
<add key="urlErreurs" value="vues/erreurs.aspx"/>
<add key="urlListe" value="vues/liste.aspx"/>
<add key="urlPanier" value="vues/panier.aspx"/>
<add key="urlPanierVide" value="vues/paniervide.aspx"/>
</appSettings>
</configuration>
للوصول إلى طبقة [business]، يمكن لفئة في طبقة [web] أن تطلب الكائن الفردي [articlesDomain]. سيقوم Spring بعد ذلك بإنشاء مثيل لكائن من النوع [istia.st.articles.domain.AchatsArticles]. لهذا الإنشاء، يحتاج إلى كائن من النوع [articlesDao]، أي كائن من النوع [istia.st.articles.dao.ArticlesDaoArrayList]. سيقوم Spring بعد ذلك بإنشاء مثيل لهذا الكائن. في نهاية العملية، تمتلك طبقة [web] التي طلبت الكائن الفردي [articlesDomain] السلسلة الكاملة التي تربطها بمصدر البيانات:
![]() |
1.5.3.3. التغييرات المتعلقة بنظام إدارة قواعد البيانات (DBMS) أو قاعدة البيانات
سيتم تجاهل هذه النقطة هنا لأننا نعمل في تطبيق اختباري بدون نظام إدارة قواعد البيانات. سنتناول تنفيذ طبقة [dao] استنادًا إلى نظام إدارة قواعد البيانات في قسم لاحق.
1.5.4. مكتبة العلامات <asp:>
لننظر إلى طريقة العرض [ERRORS]، التي تعرض قائمة بالأخطاء:

عرض [ERRORS] مسؤول عن عرض قائمة بالأخطاء التي وضعها وحدة التحكم [main.aspx] في سياق الطلب تحت الاسم [context.Items("errors")]. هناك عدة طرق لكتابة مثل هذه الصفحة. هنا، نحن مهتمون فقط بجزء عرض الأخطاء.
تذكر أن صفحة ASPX تحتوي على طبقة عرض HTML وطبقة كود .NET تقوم بإعداد البيانات التي يجب أن تعرضها طبقة العرض. يمكن أن يكون هذان الجزءان في نفس ملف [aspx] (حل WebMatrix) أو في ملفين: [aspx] للعرض، و[aspx.vb] للكود. الحل الأخير هو الذي يستخدمه Visual Studio. ولزيادة الأمور تعقيدًا، يمكن أن تحتوي طبقة العرض HTML أيضًا على كود .NET، مما يؤدي إلى طمس الفصل بين طبقتي [التحكم] و[العرض] في العرض. لا يُنصح بهذا النهج بشكل عام. تتطلب إزالة كل الكود من قسم [العرض] إنشاء مكتبات علامات. تعمل هذه المكتبات على "إخفاء" الكود تحت ستار علامات مشابهة لعلامات HTML. نقدم حلين ممكنين لصفحة [ERRORS].
يستخدم الحل الأول كود .NET في قسم [العرض] من الصفحة. تسترد صفحة ASPX قائمة الأخطاء الموجودة في الطلب في قسم وحدة التحكم [errors.aspx.vb]:
Protected erreurs As ArrayList
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
...
' error recovery
erreurs = CType(context.Items("erreurs"), ArrayList)
End Sub
ثم عرضها في قسم [presentation, errors.aspx]:
<h2>Les erreurs suivantes se sont produites :</h2>
<ul>
<%
for i as integer=0 to erreurs.count-1
response.write("<li>" & erreurs(i).ToString & "</li>")
next
%>
</ul>
يستخدم الحل الثاني العلامة <asp:repeater> من مكتبة علامات <asp:> في ASP.NET. إذا قمت بإنشاء صفحة ASPX بيانياً، فستكون هذه العلامة متاحة كعنصر تحكم خادم يمكنك سحبه إلى نموذج التصميم. إذا قمت بكتابة كود ASPX يدوياً، فيمكنك الرجوع إلى مكتبة العلامات.
باستخدام مكتبة علامات <asp:>، يصبح كود ASPX لعرض [ERRORS] السابق كما يلي:
<asp:Repeater id="rptErreurs" runat="server">
<HeaderTemplate>
<h3>Les erreurs suivantes se sont produites :
</h3>
<ul>
</HeaderTemplate>
<ItemTemplate>
<li>
<%# Container.DataItem %>
</li>
</ItemTemplate>
<FooterTemplate>
</ul>
</FooterTemplate>
</asp:Repeater>
ال
تُستخدم العلامة لتكرار قالب HTML عبر العناصر المختلفة في مصدر البيانات. وفيما يلي عناصرها المختلفة:
قالب HTML الذي سيتم عرضه قبل عرض عناصر مصدر البيانات | |
قالب HTML الذي يتكرر لكل عنصر في مصدر البيانات. يُستخدم التعبير [<%# Container.DataItem %>] لعرض قيمة العنصر الحالي في مصدر البيانات | |
قالب HTML الذي سيتم عرضه بعد عرض عناصر مصدر البيانات |
يتم ربط مصدر البيانات بالعلامة، عادةً في قسم [controller] من الصفحة:
Protected WithEvents rptErreurs As System.Web.UI.WebControls.Repeater
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
..
' link errors to rptErreurs
With rptErreurs
.DataSource = context.Items("erreurs")
.DataBind()
End With
End Sub
يمكن أيضًا إجراء هذا الربط أثناء تصميم الصفحة إذا كان مصدر البيانات معروفًا بالفعل، مثل قاعدة بيانات موجودة.
في طرق العرض الخاصة بنا، سنستخدم علامة أخرى: <asp:datagrid>، والتي تسمح لنا بعرض مصدر البيانات في شكل جدول.
1.5.5. هيكل حل Visual Studio لتطبيق [webarticles]
تطبيق الويب عبارة عن لغز مكون من العديد من القطع. وعادةً ما يؤدي تزويده بهيكل MVC إلى زيادة عدد هذه القطع. وفيما يلي هيكل تطبيق [webarticles] في [Visual Studio]:
![]() | ![]() | ![]() |
![]() |
تعليقات:
- مشروع [الويب] هو من نوع [مكتبة الفئات] وليس من نوع [تطبيق ويب ASP.NET]، كما قد يتوقع المرء منطقياً. يتطلب نوع [تطبيق ويب ASP.NET] وجود خادم الويب IIS على جهاز التطوير أو على جهاز بعيد. لا يتم تضمين خادم IIS بشكل افتراضي على أجهزة Windows XP Home Edition. ومع ذلك، يتم بيع العديد من أجهزة الكمبيوتر الشخصية مع هذا الإصدار. للسماح للقراء الذين يستخدمون Windows XP بتنفيذ التطبيق قيد الدراسة، سنستخدم خادم الويب Cassini (انظر الملحق)، المتاح مجانًا من Microsoft، وسنستبدل مشروع [تطبيق ويب ASP.NET] بمشروع [مكتبة فئات]. ينطوي هذا على بعض العيوب، والتي يتم شرحها في الملاحق.
- ملفات DLL المستخدمة من قبل التطبيق هي كما يلي:
تحتوي على فئات طبقة الوصول إلى البيانات | |
يحتوي على فئات طبقة الأعمال | |
يحتوي على فئات Spring التي تسمح لنا بدمج طبقات الويب والمجال وDAO | |
فئات التسجيل — التي يستخدمها Spring |
يتم وضع ملفات DLL هذه في مجلد [bin] وإضافتها إلى مراجع المشروع.
1.5.6. طرق عرض ASPX
كما أوصينا سابقًا، سنستخدم مكتبة العلامات <asp:> في طرق عرض ASPX الخاصة بنا.
1.5.6.1. مكون المستخدم [entete.ascx]
لضمان الاتساق عبر طرق العرض المختلفة، ستشترك جميعها في نفس الرأس، الذي يعرض اسم التطبيق جنبًا إلى جنب مع القائمة:
![]() | ![]() |
القائمة ديناميكية ويتم تعيينها بواسطة وحدة التحكم. تتضمن وحدة التحكم سمة مفتاح "actions" في الطلب المرسل إلى صفحة ASPX، مع قيمة مرتبطة بها عبارة عن مصفوفة من عناصر Hashtable(). كل عنصر في هذه المصفوفة هو قاموس يهدف إلى إنشاء خيار قائمة رأس. يحتوي كل قاموس على مفتاحين:
- href: عنوان URL المرتبط بخيار القائمة
- رابط: نص القائمة
سنحول العنوان إلى عنصر تحكم مستخدم. يقوم عنصر التحكم المستخدم بتغليف جزء من الصفحة (التخطيط والرمز المرتبط) في مكون يمكن إعادة استخدامه لاحقًا في صفحات أخرى. هنا، نريد إعادة استخدام مكون [entete] في طرق عرض أخرى للتطبيق. سيكون كود العرض في [entete.ascx] وكود عنصر التحكم المرتبط به في [entete.ascx.vb]. سيستخدم كود العرض مكون <asp:repeater> لعرض جدول خيارات القائمة:
![]() |
رقم | النوع | الاسم | الدور |
1 | مكرر | rptMenu مصدر البيانات: مصفوفة من القواميس مع مفتاحين: href، link | عرض خيارات القائمة |
سيكون كود عرض الصفحة كما يلي:
تعليقات:
- يتم تعريف مكون [repeater] في الأسطر 6–14
- كل عنصر في مصدر البيانات المرتبط بـ repeater هو قاموس يحتوي على مفتاحين: href (السطر 9) و link (السطر 10)
سيكون رمز التحكم المرتبط كما يلي:
تعليقات:
- يحتوي المكون [EnteteWebArticles] على خاصية [actions] عامة وقابلة للكتابة فقط - السطر 7
- تسمح هذه الخاصية بربط مكون <asp:repeater> المسمى [rptMenu] — السطر 10 — بمصفوفة الخيارات التي يحسبها وحدة التحكم في التطبيق — السطران 11–12.
ستستخدم طرق العرض الأخرى في التطبيق الرأس المحدد بواسطة [entete.ascx]. على سبيل المثال، ستتضمن الصفحة [erreurs.aspx] الرأس باستخدام الكود التالي:
تعليقات:
- يحدد السطر 1 أن العلامة <WA:entete> يجب أن تكون مرتبطة بالمكون المحدد بواسطة الملف [entete.ascx]. وتعتبر السمتان [TagPrefix] و[TagName] اختياريتين.
- بمجرد الانتهاء من ذلك، يتم إدراج المكون في كود عرض الصفحة باستخدام السطر 9. في وقت التشغيل، ستقوم هذه العلامة بتضمين الكود من صفحة [entete.ascx] في صفحة ASPX التي تحتوي عليها. سيتولى كود عنصر التحكم [erreurs.aspx.vb] تهيئة هذا المكون. ويمكنه القيام بذلك على النحو التالي:
تعليقات:
- السطر 6 ينشئ كائنًا من النوع [EnteteWebArticles]، وهو نوع المكون الذي يتم إنشاؤه
- السطر 11 يقوم بتهيئة الخاصية [actions] لهذا الكائن
1.5.6.2. طريقة العرض [liste.aspx]
1.5.6.2.1. مقدمة
تعرض هذه الشاشة قائمة العناصر المتاحة للبيع:
![]() | ![]() |
يتم عرضها بعد إرسال طلب إلى /main?action=list أو /main?action=cartvalidation. عناصر طلب وحدة التحكم هي كما يلي:
كائن Hashtable() - مصفوفة خيارات القائمة | |
ArrayList من الكائنات من النوع [Item] | |
كائن String - الرسالة المراد عرضها في أسفل الصفحة |
يحتوي كل رابط [Info] في جدول المقالات بتنسيق HTML على عنوان URL بالصيغة [?action=info&id=ID]، حيث يمثل ID حقل المعرف للمقال المعروض.
1.5.6.2.2. مكونات الصفحة
![]() |
لا. | النوع | الاسم | الدور |
مكون المستخدم | رأس | عرض العنوان | |
DataGrid | DataGridArticles 3 - عمود مرتبط: العنوان: الاسم، الحقل: name 4 - العمود ذو الصلة: العنوان: السعر، الحقل: السعر 5 - عمود النص التشعبي: النص: معلومات، حقل URL: id، تنسيق URL: /webarticles/main.aspx?action=info&id={0} | عرض العناصر المعروضة للبيع | |
تسمية | lblMessage | عرض رسالة |
دعونا نستعرض كيفية تعيين هذه الخصائص:
- في Visual Studio، حدد [DataGrid] للوصول إلى ورقة خصائصه:

- استخدم رابط [AutoFormat] أعلاه لإدارة تخطيط الشبكة المعروضة
- والرابط [Property Generator] لإدارة محتواها
1.5.6.2.3. كود العرض [liste.aspx]
تعليقات:
- السطر 9 يحدد رأس الصفحة
- الأسطر 12-24 تحدد خصائص [DataGrid]
- السطر 26 يحدد التسمية [lblMessage]
1.5.6.2.4. كود وحدة التحكم [liste.aspx.vb]
تعليقات:
- تظهر مكونات الصفحة في الأسطر 13-15. لاحظ أننا احتجنا إلى إنشاء كائن [EnteteWebArticles] باستخدام عامل [new]، في حين أن هذا لم يكن ضروريًا مع المكونات الأخرى. بدون هذا الإنشاء الصريح، واجهنا خطأً في وقت التشغيل يشير إلى أن كائن [entete] لم يشير إلى أي شيء. هذه النقطة تستدعي مزيدًا من التحقيق. لم يتم التحقيق فيها.
- يتم استرداد جدول خيارات قائمة العناوين من السياق لتهيئة مكون [entete] للصفحة — السطر 20
- يتم أخذ قائمة المقالات من السياق — السطر 22
- لتهيئة مكون [DataGridArticles] — الأسطر 24–27
- يتم تهيئة مكون [lblMessage] برسالة موضوعة في السياق — السطر 29
1.5.6.3. طريقة العرض [infos.aspx]
1.5.6.3.1. مقدمة
تعرض طريقة العرض هذه معلومات حول عنصر ما وتسمح أيضًا بشرائه:

يتم عرضه بعد طلب /main?action=infos&id=ID أو طلب /main?action=achat&id=ID عندما تكون الكمية المشتراة غير صحيحة. معلمات طلب وحدة التحكم هي كما يلي:
كائن Hashtable() - مصفوفة خيارات القائمة | |
كائن من النوع [Article] - العنصر المراد عرضه | |
كائن سلسلة - الرسالة المراد عرضها في حالة حدوث خطأ في الكمية | |
كائن سلسلة - القيمة المراد عرضها في حقل الإدخال [Qty] |
يتم استخدام حقول [msg] و [qte] في حالة حدوث خطأ في الإدخال المتعلق بالكمية:

تحتوي هذه الصفحة على نموذج يتم إرساله عبر زر [شراء]. عنوان URL الهدف لطلب POST هو [?action=purchase&id=ID]، حيث ID هو معرف العنصر الذي تم شراؤه.
1.5.6.3.2. مكونات الصفحة
![]() |
رقم | النوع | الاسم | الدور |
مكون المستخدم | رأس | عرض العنوان | |
حرف | رقم العنصر | عرض رقم العنصر | |
DataGrid | DataGridArticle 3 - العمود ذو الصلة: العنوان: الاسم، الحقل: name 4 - العمود ذو الصلة: العنوان: السعر، الحقل: السعر 5 - العمود ذو الصلة: العنوان: المخزون الحالي، الحقل: currentStock 6 - العمود ذو الصلة: العنوان: الحد الأدنى للمخزون، الحقل: stockMinimum | عرض عنصر | |
إرسال HTML | إرسال النموذج | ||
إدخال HTML runat=server | txtQty | أدخل الكمية المشتراة | |
label | lblMsgQte | أي رسالة خطأ |
1.5.6.3.3. رمز العرض [infos.aspx]
تعليقات:
- تم تضمين الرأس في الصفحة - السطر 9
- تم تعريف القيمة الثابتة [litId] في السطر 10
- يتم تعريف DataGrid [DataGridArticles] في الأسطر 12–34
- يتم تعريف النموذج في الأسطر 36-46. وهو من النوع POST.
- يتم توفير هدف POST بواسطة متغير [strAction] - السطر 36. يجب تعريف هذا المتغير بواسطة وحدة التحكم.
- يتم تعريف حقل الإدخال الخاص بالكمية المشتراة في السطر 41. وهو مكون HTML من جانب الخادم (runat=server). من ناحية الكود، يتم الوصول إليه عبر كائن.
- يحدد السطر 42 التسمية [lblMsgQte]، التي ستحتوي على أي رسائل خطأ تتعلق بالكمية المدخلة
1.5.6.3.4. كود التحكم [infos.aspx.vb]
تعليقات:
- يتم تعريف مكونات الصفحة في الأسطر 10–14
- تحدد الفئة خاصية عامة [strAction] تُستخدم لتحديد هدف POST للنموذج - الأسطر 17-25
- يتم استرداد المقالة المراد عرضها من سياق التطبيق - السطر 30
- يتم استرداد مصفوفة خيارات قائمة الرأس من السياق لتهيئة مكون [entete] للصفحة - السطر 32
- الأسطر 33-39: يتم ربط مكون [DataGridArticle] بمصدر بيانات من النوع [ArrayList] يحتوي فقط على المقالة التي تم استردادها في السطر 30
- يتم تهيئة مكونات [lblMsgQte، txtQte] بالمعلومات المأخوذة من السياق - الأسطر 42-45
- يتم أيضًا تهيئة الخاصية [straction] بمعلومات مأخوذة من السياق — السطر 47. تُستخدم هذه المتغير لإنشاء السمة [action] لنموذج HTML الموجود على الصفحة:
1.5.6.4. عرض [panier.aspx]
1.5.6.4.1. مقدمة
تعرض طريقة العرض هذه محتويات سلة التسوق:

يتم عرضه استجابة لطلب مثل /main?action=cart أو /main?action=cancelpurchase&id=ID. معلمات طلب وحدة التحكم هي كما يلي:
كائن Hashtable() - مصفوفة خيارات القائمة | |
كائن من النوع [Cart] - سلة التسوق المراد عرضها |
يحتوي كل رابط [إزالة] في جدول HTML لعناصر سلة التسوق على عنوان URL بالصيغة [?action=removeitem&id=ID]، حيث يمثل ID حقل [id] للعنصر المراد إزالته من السلة.
1.5.6.4.2. مكونات الصفحة
![]() |
رقم | النوع | الاسم | الدور |
مكون المستخدم | رأس | عرض العنوان | |
DataGrid | DataGridPurchases 3 - عمود مرتبط - العنوان: العنصر، الحقل: الاسم 4 - عمود مرتبط - العنوان: الكمية، الحقل: الكمية 5 - عمود مرتبط - العنوان: السعر، الحقل: السعر 6 - عمود مرتبط - العنوان: المجموع، الحقل: المجموع، التنسيق {0:C} 7 - عمود النص التشعبي - النص: إزالة، عنوان URL: id، تنسيق عنوان URL: /webarticles/main.aspx?action=retirerachat&id={0} | عرض قائمة العناصر المشتراة | |
تسمية | lblTotal | عرض المبلغ المستحق |
1.5.6.4.3. كود العرض [panier.aspx]
تعليقات
- السطر 9 يتضمن العنوان
- الأسطر 12–27 تحدد مكون [DataGridAchats]
- السطر 29: يتم تعريف المكون [lblTotal]
1.5.6.4.4. كود عنصر التحكم [panier.aspx.vb]
تعليقات:
- يتم تعريف مكونات الصفحة في الأسطر 11–13
- تكون تهيئة مكون [header] مطابقة لتلك الموجودة في الصفحات التي تمت دراستها سابقًا — السطر 17
- يتم استرداد عربة التسوق المراد عرضها من الجلسة — السطر 19
- يُشكل عرض عربة التسوق هذه باستخدام مكون [DataGridAchats] مشكلة. تنبع الصعوبة من تهيئة المكون. دعونا نستعرض أعمدة المكون:
- عمود [Article] المرتبط بحقل [name] في مصدر البيانات
- عمود [Qty] المرتبط بحقل [qty] في مصدر البيانات
- عمود [Price] المرتبط بحقل [price] في مصدر البيانات
- عمود [Total] المرتبط بحقل [total] في مصدر البيانات
مصدر البيانات الذي لدينا هو عربة التسوق وقائمة التسوق الخاصة بها. وستكون هذه الأخيرة بمثابة مصدر البيانات لـ [DataGrid]. ومع ذلك، فإن كائنات [Purchase] التي ستملأ صفوف [DataGrid] لا تحتوي على الخصائص [name, qty, price, total] التي يتوقعها [DataGrid]. لذلك، نقوم هنا، خصيصًا لـ [DataGrid]، بإنشاء مصدر بيانات تحتوي عناصره على الخصائص التي يتوقعها [DataGrid]. ستكون هذه العناصر من النوع [PurchaseLine]، وهي فئة تم إنشاؤها لهذا الغرض ومشتقة من فئة [Purchase] — الأسطر 36–66
- بمجرد تعريف فئة [PurchaseLine]، يتم إنشاء مصدر البيانات لـ [DataGridPurchases] من سلة التسوق الموجودة في الجلسة — الأسطر 20–30
- يتم عرض إجمالي مبلغ الشراء باستخدام خاصية [totalPanier] للفئة [Panier] — السطر 32
1.5.6.5. طريقة العرض [emptyCart.aspx]
1.5.6.5.1. مقدمة
تعرض طريقة العرض هذه معلومات تشير إلى أن عربة التسوق فارغة:

يتم عرضها بعد إرسال طلب إلى /main?action=panier أو /main?action=retirerachat&id=ID. وفيما يلي معلمات طلب وحدة التحكم:
كائن Hashtable() - مصفوفة خيارات القائمة |
1.5.6.5.2. مكونات الصفحة
![]() |
رقم | النوع | الاسم | الدور |
مكون المستخدم | رأس | عرض العنوان |
1.5.6.5.3. كود العرض [emptycart.aspx]
تعليقات:
- تم تضمين الرأس في السطر 9
1.5.6.5.4. رمز التحكم [paniervide.aspx.vb]
تعليقات:
- نقوم ببساطة بتهيئة المكون الديناميكي الوحيد للصفحة - السطر 10
1.5.6.6. طريقة العرض [errors.aspx]
1.5.6.6.1. مقدمة
يتم عرض هذا العرض في حالة حدوث أخطاء:

يتم عرضه بعد أي طلب ينتج عنه خطأ، باستثناء عملية الشراء بكمية غير صحيحة، والتي يتم التعامل معها من خلال عرض [INFOS]. عناصر طلب وحدة التحكم هي كما يلي:
كائن Hashtable() - مصفوفة خيارات القائمة | |
مجموعة ArrayList من كائنات [String] تمثل رسائل الخطأ المراد عرضها |
1.5.6.6.2. مكونات الصفحة
![]() |
رقم | النوع | الاسم | الدور |
مكون المستخدم | رأس | عرض العنوان | |
المكرر | rptErrors | عرض قائمة الأخطاء |
1.5.6.6.3. كود العرض [errors.aspx]
تعليقات:
- يتم تعريف الرأس في السطر 9
- يتم تعريف المكون [rptErrors] في الأسطر 13–19. ويأتي محتواه من مصدر بيانات من النوع [ArrayList] يحتوي على كائنات [String].
1.5.6.6.4. كود عنصر التحكم [errors.aspx.vb]
تعليقات:
- يتم تهيئة المكون [header] كالمعتاد، في السطرين 9 و 14
- يتم تهيئة مكون [rptErrors] باستخدام قائمة الأخطاء [ArrayList] الموجودة في السياق — الأسطر 16–19
1.5.7. وحدات التحكم global.asax و main.aspx
لا يزال يتعين علينا كتابة جوهر تطبيق الويب الخاص بنا: وحدة التحكم. وتتمثل مهمتها في:
- استرداد طلب العميل،
- معالجة الإجراء المطلوب من قبل العميل باستخدام فئات الأعمال،
- إرسال العرض المناسب كاستجابة.
1.5.7.1. وحدة التحكم [global.asax.vb]
عندما يتلقى التطبيق طلبه الأول، يتم تنفيذ الإجراء [Application_Start] في ملف [global.asax.vb]. سيحدث هذا مرة واحدة فقط. الغرض من الإجراء [Application_Start] هو تهيئة الكائنات المطلوبة من قبل تطبيق الويب، والتي سيتم مشاركتها في وضع القراءة فقط من قبل جميع مؤشرات ترابط العميل. يمكن وضع هذه الكائنات المشتركة في موقعين:
- الحقول الخاصة بوحدة التحكم
- سياق تنفيذ التطبيق (Application)
ستقوم طريقة [Application_Start] في ملف [global.asax.vb] بتنفيذ الإجراءات التالية:
- التحقق من ملف [web.config] بحثًا عن المعلمات اللازمة لتشغيل التطبيق بشكل صحيح. وقد تم وصف هذه المعلمات في القسم 1.5.3.
- وضع قائمة بأي أخطاء في سياق التطبيق في شكل كائن [ArrayList errors]. ستكون هذه القائمة فارغة في حالة عدم وجود أخطاء، ولكنها ستظل موجودة على أي حال.
- في حالة وجود أخطاء، تتوقف طريقة [Application_Start] عند هذا الحد. وإلا، فإنه يطلب مرجعًا إلى عنصر فريد من نوع [IArticlesDomain]، والذي سيكون كائن الأعمال الذي سيستخدمه وحدة التحكم لتلبية احتياجاته. كما هو موضح في 1.5.3.2، ستطلب وحدة التحكم هذا العنصر الفريد من إطار عمل Spring. قد تؤدي عملية إنشاء المثيل هذه إلى أخطاء متنوعة. إذا كان الأمر كذلك، فسيتم تخزين هذه الأخطاء مرة أخرى في كائن [errors] لسياق التطبيق.
يحتوي وحدة التحكم [global.asax.vb] على إجراء [Session_Start] الذي يتم تشغيله في كل مرة يصل فيها عميل جديد. في هذا الإجراء، سننشئ عربة تسوق فارغة للعميل. سيتم الحفاظ على عربة التسوق هذه طوال طلبات هذا العميل بالذات. قد يكون الكود كما يلي:
تعليقات:
- يتم تعريف المعلمات المتوقعة في [web.config] في صفيفة - السطر 18
- يتم البحث عنها في [web.config]. إذا كانت موجودة، يتم تخزينها في سياق التطبيق؛ وإلا، يتم تسجيل خطأ في قائمة الأخطاء [errors] - الأسطر 21-33
- إذا لم تكن هناك أخطاء، يُطلب من Spring مرجع إلى العنصر الفردي [articlesDomain]، الذي يدير الوصول إلى طبقة [domain] للتطبيق - الأسطر 35-47. يتم تسجيل أي أخطاء في [errors].
- يتم تسجيل الأخطاء في سياق التطبيق - السطر 49
- يتم إنهاء الإجراء في حالة وجود أي أخطاء - السطر 51
- نقوم بإنشاء مصفوفة من ثلاثة قواميس. يحتوي كل منها على مفتاحين: href و link. تمثل هذه المصفوفة خيارات القائمة الثلاثة المحتملة — الأسطر 52–71
- يتم تخزين هذه المصفوفة في سياق التطبيق - السطر 73
- يتم تنفيذ الإجراء [Session_Start] لكل عميل جديد. يتم إنشاء سلة تسوق فارغة في جلسة عمل العميل - الأسطر 78-81
1.5.7.2. وحدة التحكم [main.aspx.vb]
تتعامل وحدة التحكم [main.aspx.vb] مع جميع طلبات العملاء. تتبع جميع هذه الطلبات التنسيق [/webarticles/main.aspx?action=XX]. تتم معالجة الطلب على النحو التالي:
- يتم فحص الكائن [errors] في سياق التطبيق. إذا لم يكن فارغًا، فهذا يعني حدوث أخطاء أثناء تهيئة التطبيق وأن التطبيق لا يمكن تشغيله. استجابةً لذلك، يتم إرسال عرض [ERRORS].
- يتم استرداد المعلمة [action] للطلب وفحصها. إذا لم تتطابق مع إجراء معروف، يتم إرسال عرض [ERRORS] مع رسالة خطأ مناسبة.
- إذا كانت المعلمة [action] صالحة، يتم تمرير طلب العميل إلى إجراء خاص بهذا الإجراء للمعالجة:
method | الطلب | المعالجة | الاستجابات المحتملة |
GET /main?action=list | - طلب قائمة العناصر من فئة الأعمال - عرضها | [LIST] أو [ERRORS] | |
GET /main?action=info&id=ID | - استرداد العنصر ذي المعرف id=ID من فئة الأعمال - عرضه | [INFO] أو [ERRORS] | |
POST /main?action=purchase&id=ID - الكمية المشتراة مضمنة في المعلمات المرسلة | - طلب العنصر ذي الرقم التعريفي id=ID من فئة الأعمال - أضفه إلى سلة التسوق في جلسة عمل العميل | [LIST] أو [INFO] أو [ERRORS] | |
GET /main?action=removePurchase&id=ID | - إزالة العنصر ذي المعرف id=ID من قائمة التسوق في جلسة عمل العميل | [CART] | |
GET /main?action=cart | - عرض جلسة العميل | [CART] أو [EMPTY_CART] | |
GET /main?action=cartvalidation | - خفض مستويات المخزون لجميع العناصر في [LIST] أو [ERRORS] | [LIST] أو [ERRORS] |
قد يبدو قالب وحدة التحكم [main.aspx.vb] كما يلي:
تعليقات:
- تحتوي الفئة على حقلين خاصين سيتم مشاركتهما بين الطرق — السطور 15–16:
- articlesDomain: العنصر الفردي للوصول إلى طبقة [domain]
- options: مصفوفة القواميس التي تحتوي على خيارات القائمة
- الإجراء [Page_Load]:
- سيقوم بتهيئة الحقلين الخاصين للفئة
- يسترد المعلمة [action] من الطلب وينفذ الأسلوب الذي يتعامل مع هذا الإجراء.
1.5.7.3. طريقة [Page_Load]
هذا الحدث هو أول ما يحدث على الصفحة. الرمز كما يلي:
تعليقات:
- في كل مرة يتم فيها تحميل الصفحة، نتأكد من نجاح تهيئة التطبيق التي يتم إجراؤها بواسطة [global.asax].
- للقيام بذلك، نسترد قائمة الأخطاء التي وضعها [global.asax] هناك من سياق التطبيق - السطر 4
- إذا لم تكن هذه القائمة فارغة، فإننا نعرض عرض [ERRORS] — الأسطر 6–10
- نسترد العنصر الفردي [articlesDomain] الذي وضعه [global.asax] في سياق التطبيق ونخزنه في الحقل الخاص [articlesDomain] بحيث يكون متاحًا لمختلف أساليب الفئة — السطر 12
- نقوم بعملية مماثلة مع مصفوفة خيارات القائمة - السطر 14
- استرداد المعلمة [action] من الطلب - السطر 16
- نقوم بتنفيذ الطريقة المطابقة للإجراء المطلوب. يتم التعامل مع الإجراء غير المحدد على أنه الإجراء [list] — الأسطر 16-36
1.5.7.4. معالجة الإجراء [list]
يتضمن ذلك عرض قائمة العناصر:

والكود هو كما يلي:
تعليقات:
- يتم وضع أي أخطاء في [ArrayList] - السطر 4
- يتم طلب قائمة العناصر من العنصر الفردي [articlesDomain] - الأسطر 5-12
- في حالة وجود أخطاء، يتم عرض [ERRORS] - الأسطر 13-19
- وإلا، يتم إرجاع عرض [LIST] - الأسطر 20-24
1.5.7.5. معالجة الإجراء [info]
طلب العميل معلومات حول عنصر معين:

والكود هو كما يلي:
تعليقات:
- يتم وضع أي أخطاء في [ArrayList] - السطر 4
- يتم استرداد معرف العنصر المطلوب من الطلب - السطر 6
- يتم التحقق من صحة هذا المعرف. يجب أن يكون موجودًا ويجب أن يكون عددًا صحيحًا. إذا لم يكن كذلك، يتم عرض طريقة العرض [ERRORS] مع رسالة الخطأ المناسبة - الأسطر 7-27
- بمجرد التحقق من المعرف، يتم طلب العنصر من العنصر الفردي [articlesDomain]. في حالة حدوث استثناء، يتم إرسال عرض [ERRORS] - الأسطر 29-39
- إذا لم يتم العثور على العنصر، يتم إرسال عرض [ERRORS] — الأسطر 41–48
- إذا تم العثور على العنصر، يتم وضعه في جلسة عمل المستخدم ثم عرضه في عرض [INFO] - الأسطر 50-56
1.5.7.6. معالجة إجراء [purchase]
قام العميل بشراء العنصر المعروض في عرض [INFO].

والرمز هو كما يلي:
تعليقات:
- يتم استرداد العنصر الموجود في الجلسة - السطر 5
- إذا لم يكن موجودًا (ربما انتهت صلاحية الجلسة)، يتم عرض طريقة العرض [LIST] - الأسطر 7-10
- يتم استرداد الكمية المشتراة من الاستعلام - السطر 12
- يتم التحقق من صلاحيتها - الأسطر 13-29
- إذا كانت غير صالحة، فحسب الحالة، يتم عرض طريقة العرض [LIST] - السطر 16 أو طريقة العرض [INFO] - الأسطر 24-28
- إذا كان كل شيء طبيعيًا، تتم إضافة الشراء إلى سلة التسوق - الأسطر 31-32
- ثم يتم إرسال عرض [LIST] - السطر 34
1.5.7.7. معالجة إجراء [cart]
قام العميل بعدة عمليات شراء ويريد عرض سلة التسوق:

والكود كالتالي:
تعليقات:
- نسترد سلة التسوق من الجلسة - السطر 4. لا نتحقق هنا للتأكد من أننا نسترد شيئًا بالفعل. يجب أن نفعل ذلك لأن الجلسة قد تكون انتهت صلاحيتها.
- إذا كانت سلة التسوق فارغة، نرسل عرض [EMPTY CART] - الأسطر 6-10
- وإلا، نرسل عرض [SHOPPINGCART] — الأسطر 11–14
1.5.7.8. معالجة الإجراء [removepurchase]
يريد العميل إزالة عملية شراء من سلة التسوق الخاصة به:

الرمز كما يلي:
تعليقات:
- استرداد سلة التسوق من الجلسة - السطر 4. لا نتحقق هنا للتأكد من أن شيئًا ما قد تم استرداده بالفعل. يجب القيام بذلك لأن الجلسة قد تكون انتهت صلاحيتها.
- استرداد معرف [id] العنصر المراد إزالته من الاستعلام - السطر 8.
- يتم إزالة عملية الشراء المقابلة من سلة التسوق - السطر 10
- لم يتم التحقق هنا من صحة معرف العنصر الذي تم شراؤه. إذا كان نوعه غير صالح، فسيحدث استثناء وسيتم التعامل معه في الأسطر 11-13. إذا كان صالحًا ولكنه غير موجود، فإن طريقة [cart.remove] — السطر 10 — لا تفعل شيئًا.
- يتم عرض سلة التسوق الجديدة - السطر 16
1.5.7.9. معالجة الإجراء [validateCart]
يريد العميل تأكيد سلة التسوق الخاصة به:

الرمز كما يلي:
تعليقات:
- نسترد سلة التسوق من الجلسة - السطر 6. لا نتحقق هنا للتأكد من أننا نسترد شيئًا بالفعل. يجب أن نفعل ذلك لأن الجلسة قد تكون انتهت صلاحيتها.
- إذا انتهت صلاحية الجلسة، فسيكون لدينا مؤشر [nothing] لعربة التسوق، وستقوم طريقة [buy] — السطر 9 — بإلقاء استثناء، وسيتم عرض طريقة العرض [ERRORS]. ومع ذلك، ستكون رسالة الخطأ غير واضحة للمستخدم.
- الأسطر 8–16: نحاول التحقق من صحة سلة التسوق التي تم استردادها من الجلسة. قد تفشل بعض عمليات الشراء إذا تجاوزت الكمية المطلوبة مخزون العنصر المطلوب. يتم تخزين هذه الحالات بواسطة طريقة [buy] في قائمة أخطاء، والتي يتم استردادها في السطر 11.
- في حالة وجود أخطاء، يتم إرسال عرض [ERRORS] — الأسطر 18–23
- وإلا، يتم إرسال عرض [LIST] — الأسطر 25–26
1.6. الخلاصة
لقد قمنا هنا بتطوير تطبيق باستخدام نمط MVC. حتى أبريل 2005، لا يبدو أن هناك أي أطر عمل احترافية لتطوير MVC لـ ASP.NET يمكن مقارنتها بتلك المتاحة لـ Java (Struts، Spring، إلخ). ومن المتوقع أن يصدر مشروع [Spring.net] واحدة قريبًا. وحتى ذلك الحين، توفر الطريقة الموضحة أعلاه نهجًا قابلاً للتطبيق لتطوير MVC للتطبيقات متوسطة الحجم.































