8. أحداث المستخدم
تناولنا في الفصل السابق مفهوم الأحداث المرتبطة بمكونات النموذج. سنرى الآن كيفية إنشاء أحداث في فئاتنا الخاصة.
8.1. كائنات delegate المحددة مسبقًا
لقد تناولنا مفهوم الكائن delegate في الفصل السابق، ولكننا لم نتعمق فيه آنذاك. عندما نظرنا في كيفية إعلان معالجات أحداث مكونات النموذج، صادفنا كودًا مشابهًا لما يلي:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
حيث كان buttonAfficher مكونًا من النوع [Button]. تحتوي هذه الفئة على حقل Click محدد على النحو التالي:
![]() |
- [1]: الفئة [Button]
- [2]: أحداثها
- [3,4]: الحدث Click
- [5]: إعلان الحدث [Control.Click] [4].
- EventHandler هو نموذج أولي (قالب) لطريقة تسمى delegate.
- event هي كلمة رئيسية تقيد وظائف delegate EventHandler: يتمتع كائن delegate بوظائف أكثر ثراءً من كائن event.
يتم تعريف delegate EventHandler على النحو التالي:
![]() |
يشير delegate EventHandler إلى نموذج طريقة:
- الذي يكون المعلمة الأولى له من نوع Object
- والمعلمة الثانية من النوع EventArgs
- لا يعطي أي نتيجة
قد تكون الطريقة المطابقة للنموذج المحدد بواسطة EventHandler كما يلي:
private void buttonAfficher_Click(object sender, EventArgs e);
لإنشاء كائن من النوع EventHandler، يتم اتباع الخطوات التالية:
EventHandler evtHandler=new EventHandler(méthode correspondant au prototype défini par le type EventHandler);
وبذلك يمكن كتابة:
المتغير من النوع delegate هو في الواقع قائمة من المراجع حول الطرق المطابقة لنموذج delegate. لإضافة طريقة جديدة M إلى المتغير evtHandler أعلاه، نستخدم الصيغة:
يمكن استخدام الرمز += حتى لو كانت evtHandler قائمة فارغة.
الأمر:
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);
يُضيف طريقة من النوع EventHandler إلى قائمة طرق الحدث buttonAfficher.Click. عند حدوث الحدث Click على المكون buttonAfficher، يقوم VB بتنفيذ الأمر:
حيث:
- source هو المكون من النوع object الذي تسبب في الحدث
- evt من النوع EventArgs ولا يحتوي على معلومات
جميع طرق التوقيع void M(object,EventArgs) التي تم ربطها بالحدث Click بواسطة:
this.buttonAfficher.Click += new System.EventHandler(M);
سيتم استدعاؤها باستخدام المعلمات (source، evt) التي تم تمريرها بواسطة VB.
8.2. تحديد كائنات delegate
التعليمات
public delegate int Opération(int n1, int n2);
نوعًا يسمى Opération يمثل نموذجًا أوليًا لوظيفة تقبل عددين صحيحين وتُرجع عددًا صحيحًا. الكلمة الرئيسية delegate هي التي تجعل من Opération تعريفًا لنموذج أولي لوظيفة.
وستكون وظيفة المتغير op من النوع Opération هي تسجيل قائمة من الدوال المطابقة للنموذج الأولي Opération:
يتم تسجيل طريقة fi في المتغير op بواسطة op=new Opération(fi) أو ببساطة عن طريق op=fi. لإضافة طريقة fj إلى قائمة الوظائف المسجلة بالفعل، نكتب op+= fj. لإزالة طريقة fk مسجلة بالفعل، نكتب op-=fk. إذا كتبنا في مثالنا n=op(n1,n2)، فسيتم تنفيذ جميع الطرق المسجلة في المتغير op باستخدام المعلمات n1 و n2. ستكون النتيجة المستردة n هي نتيجة آخر طريقة تم تنفيذها. لا يمكن الحصول على النتائج التي تنتجها جميع الطرق. لهذا السبب، إذا تم تسجيل قائمة بالطرق في دالة مفوضة، فإنها غالبًا ما تُرجع نتيجة من النوع void.
لنأخذ المثال التالي:
using System;
namespace Chap6 {
class Class1 {
// تعريف نموذج أولي لوظيفة
// تقبل عددين صحيحين كمعلمات وتُرجع عددًا صحيحًا
public delegate int Opération(int n1, int n2);
// طريقتان للمثيل تتوافقان مع النموذج الأولي
public int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//إضافة
public int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//طرح
// طريقة ثابتة تتوافق مع النموذج الأولي
public static int Augmenter(int n1, int n2) {
Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
return n1 + 2 * n2;
}//زيادة
static void Main(string[] args) {
// يتم تعريف كائن من نوع عملية لتسجيل الوظائف فيه
// يتم تسجيل الدالة الثابتة زيادة
Opération op = Augmenter;
// يتم تنفيذ الوكيل
int n = op(4, 7);
Console.WriteLine("n=" + n);
// إنشاء كائن c1 من نوع class1
Class1 c1 = new Class1();
// يتم تسجيل طريقة "إضافة" الخاصة بـ c1 في الوكيل
op = c1.Ajouter;
// تنفيذ الكائن الوكيل
n = op(2, 3);
Console.WriteLine("n=" + n);
// يتم تسجيل طريقة طرح c1 في الوكيل
op = c1.Soustraire;
n = op(2, 3);
Console.WriteLine("n=" + n);
//تسجيل دالتين في الوكيل
op = c1.Ajouter;
op += c1.Soustraire;
// تنفيذ الكائن المفوض
op(0, 0);
// إزالة دالة من الكائن المفوض
op -= c1.Soustraire;
// يتم تنفيذ الوكيل
op(1, 1);
}
}
}
- السطر 3: يحدد فئة Class1.
- السطر 6: تعريف delegate Opération: نموذج أولي لطرق تقبل معلمتين من النوع int وتُرجع نتيجة من النوع int
- الأسطر 9-12: طريقة المثيل Ajouter لها توقيع الوكيل Opération.
- الأسطر 14-17: طريقة المثيل Soustraire لها توقيع الوكيل Opération.
- الأسطر 20-23: طريقة الفئة Augmenter لها توقيع الوكيل Opération.
- السطر 25: تم تنفيذ الطريقة Main
- السطر 20: المتغير op هو من نوع delegate Opération. وسيحتوي على قائمة بالطرق التي تحمل توقيع نوع delegate Opération. يتم تعيين مرجع أول للطريقة له، وهو المرجع الخاص بالطريقة الثابتة Class1.Augmenter.
- السطر 31: يتم تنفيذ delegate op: سيتم تنفيذ جميع الطرق المشار إليها بواسطة op. وسيتم تنفيذها باستخدام المعلمات التي تم تمريرها إلى delegate و op. هنا، سيتم تنفيذ الطريقة الثابتة Class1.Augmenter فقط.
- السطر 35: يتم إنشاء مثيل c1 للفئة Class1.
- السطر 37: يتم تعيين طريقة المثيل c1.Ajouter إلى المندوب op. كانت Augmenter طريقة ثابتة، بينما Ajouter هي طريقة مثيل. أردنا إظهار أن هذا لا يهم.
- السطر 39: يتم تنفيذ delegate op: سيتم تنفيذ الطريقة Ajouter باستخدام المعلمات التي تم تمريرها إلى delegate op.
- السطر 42: نكرر الأمر نفسه مع طريقة المثيل Soustraire.
- السطران 46-47: يتم وضع الطريقتين Ajouter و Soustraire في delegate و op.
- السطر 49: يتم تنفيذ delegate op: سيتم تنفيذ كلتا الطريقتين Ajouter و Soustraire باستخدام المعلمات التي تم تمريرها إلى delegate و op.
- السطر 51: تمت إزالة الطريقة Soustraire من delegate و op.
- السطر 53: يتم تنفيذ delegate op: سيتم تنفيذ الطريقة المتبقية Ajouter.
نتائج التنفيذ هي كما يلي:
8.3. المندوبون أم الواجهات؟
قد تبدو مفاهيم delegates والواجهات متقاربة إلى حد ما، وقد يتساءل المرء عن الفروق الدقيقة بين هذين المفهومين. لنأخذ المثال التالي الذي يشبه مثالاً سبق دراسته:
using System;
namespace Chap6 {
class Program1 {
// تعريف نموذج أولي لوظيفة
// يقبل عددين صحيحين كمعلمات ويعيد عددًا صحيحًا
public delegate int Opération(int n1, int n2);
// طريقتان للمثيل تتوافقان مع النموذج الأولي
public static int Ajouter(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}//إضافة
public static int Soustraire(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}//طرح
// تنفيذ مندوب
public static int Execute(Opération op, int n1, int n2){
return op(n1, n2);
}
static void Main(string[] args) {
// تنفيذ الوكيل إضافة
Console.WriteLine(Execute(Ajouter, 2, 3));
// تنفيذ الوكيل طرح
Console.WriteLine(Execute(Soustraire, 2, 3));
// تنفيذ مندوب متعدد البث
Opération op = Ajouter;
op += Soustraire;
Console.WriteLine(Execute(op, 2, 3));
// إزالة وظيفة من الوكيل
op -= Soustraire;
// تنفيذ الوكيل
Console.WriteLine(Execute(op, 2, 3));
}
}
}
في السطر 20، تنتظر الطريقة Execute مرجعًا إلى كائن من نوع delegate Opération المحدد في السطر 6. وهذا يسمح بتمرير طرق مختلفة (السطور 26 و28 و32 و36) إلى الطريقة Execute. يمكن الحصول على خاصية تعدد الأشكال هذه أيضًا باستخدام واجهة:
using System;
namespace Chap6 {
// واجهة IOperation
public interface IOperation {
int operation(int n1, int n2);
}
// فئة إضافة
public class Ajouter : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
return n1 + n2;
}
}
// فئة طرح
public class Soustraire : IOperation {
public int operation(int n1, int n2) {
Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
return n1 - n2;
}
}
// فئة الاختبار
public static class Program2 {
// تنفيذ الطريقة الوحيدة للواجهة IOperation
public static int Execute(IOperation op, int n1, int n2) {
return op.operation(n1, n2);
}
public static void Main() {
// تنفيذ مندوب الإضافة
Console.WriteLine(Execute(new Ajouter(), 2, 3));
// تنفيذ مندوب الطرح
Console.WriteLine(Execute(new Soustraire(), 2, 3));
}
}
}
- الأسطر 6-8: تحدد واجهة [IOperation] طريقة operation.
- الأسطر 11-16 و19-24: تنفذ الفئتان [Ajouter] و[Soustraire] الواجهة [IOperation].
- الأسطر 29-31: الطريقة Execute التي يكون المعلمة الأولى لها من نوع واجهة IOperation. ستتلقى الطريقة Execute على التوالي كمعلمة أولى مثيلًا من الفئة Ajouter ثم مثيلًا من الفئة Soustraire.
ونلاحظ هنا الطابع المتعدد الأشكال الذي كان يتميز به المعلمة من النوع delegate في المثال السابق. ويُظهر المثالان في الوقت نفسه الاختلافات بين هذين المفهومين.
يمكن استبدال النوعين delegate و interface
- إذا كانت الواجهة تحتوي على طريقة واحدة فقط. في الواقع، النوع delegate هو غلاف لطريقة واحدة بينما يمكن للواجهة أن تحدد عدة طرق.
- إذا لم يتم استخدام جانب البث المتعدد لـ delegate. لا يوجد مفهوم البث المتعدد هذا في الواجهة.
إذا تم التحقق من هذين الشرطين، فسيكون لدينا الخيار بين التوقيعين التاليين للطريقة Execute:
int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)
قد تكون التوقيع الثاني الذي يستخدم delegate أكثر مرونة في الاستخدام. في الواقع، في التوقيع الأول، يجب أن ينفذ المعلمة الأولى للطريقة واجهة IOperation. وهذا يتطلب إنشاء فئة لتعريف الطريقة المطلوب تمريرها كمعلمة أولى إلى الطريقة Execute. في التوقيع الثاني، أي طريقة موجودة لها التوقيع الصحيح تفي بالغرض. ولا توجد حاجة إلى إنشاء أي بنية إضافية.
8.4. إدارة الأحداث
يمكن استخدام كائنات delegate لتعريف الأحداث. يمكن لفئة C1 تعريف حدث evt بالطريقة التالية:
- يتم تعريف نوع delegate داخل أو خارج الفئة C1:
- تحدد الفئة C1 حقلًا من النوع delegate Evt:
- عندما ترغب مثيل c1 من الفئة C1 في الإبلاغ عن حدث، فإنها ستنفذ مندوبها Evt1 عن طريق تمرير المعلمات المحددة بواسطة المندوب Evt إليه. سيتم عندئذٍ تنفيذ جميع الطرق المسجلة في delegate Evt1 باستخدام هذه المعلمات. يمكن القول إنها قد أُبلغت بالحدث Evt1.
- إذا أراد كائن c2 يستخدم كائن c1 أن يتم إخطاره بحدوث الحدث Evt1 على الكائن c1، فإنه سيسجل إحدى طرقه c2.M في الكائن المفوض c1.Evt1 التابع للكائن c1. وبذلك ستُنفذ طريقته c2.M في كل مرةحدث Evt1 على الكائن c1. ويمكنه أيضًا إلغاء التسجيل عندما لا يرغب في تلقي إشعارات بالحدث.
- نظرًا لأن الكائن المفوض c1.Evt1 يمكنه تسجيل عدة طرق، فإن كائنات ci المختلفة يمكنها التسجيل لدى الوكيل c1.Evt1 لتلقي إشعارات بالحدث Evt1 على c1.
في هذا السيناريو، لدينا:
- فئة تُبلغ عن حدث
- فئات يتم إخطارها بهذا الحدث. ويقال إنها تشترك في الحدث أو تنضم إليه.
- نوع delegate الذي يحدد توقيع الطرق التي سيتم إخطارها بالحدث
يحدد إطار العمل .NET ما يلي:
- توقيع قياسي لـ delegate لحدث ما
- source: الكائن الذي أبلغ عن الحدث
- evtInfo: كائن من النوع EventArgs أو مشتق منه يوفر معلومات عن الحدث
- يجب أن ينتهي اسم delegate بـ EventHandler
- طريقة قياسية لإعلان حدث من النوع MyEventHandler في فئة:
الحقل Evt1 هو من النوع delegate. الكلمة الرئيسية event موجودة لتقييد العمليات التي يمكن إجراؤها عليه:
- من خارج الفئة C1، لا يمكن إجراء سوى العمليتين += و -=. وهذا يمنع حذف (بسبب خطأ من المطور على سبيل المثال) الطرق المشتركة في الحدث. يمكن ببساطة الاشتراك (+=) أو إلغاء الاشتراك (-=) في الحدث.
- يمكن فقط لمثيل من النوع C1 تنفيذ استدعاء Evt1(source,evtInfo) الذي يطلق تنفيذ الطرق المشتركة في الحدث Evt1.
يوفر إطار العمل .NET طريقة عامة تتوافق مع توقيع delegate لحدث ما:
public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
- يستخدم delegate EventHandler النوع العام TEventArgs الذي هو نوع المعلمة الثانية
- يجب أن يكون النوع TEventArgs مشتقًا من النوع EventsArgs (حيث TEventArgs: EventArgs)
باستخدام هذا النوع العام delegate، سيتبع إعلان حدث X في الفئة C المخطط الموصى به التالي:
- تحديد نوع XEventArgs مشتق من EventArgs لتغليف المعلومات حول الحدث X
- تحديد في الفئة C حدث من النوع EventHandler<XEventArgs>.
- تحديد في الفئة C طريقة محمية
مخصصة لـ "نشر" الحدث X للمشتركين.
لنأخذ المثال التالي:
- تغلف الفئة Emetteur درجة حرارة. يتم رصد هذه الدرجة. عندما تتجاوز هذه الدرجة عتبة معينة، يجب إطلاق حدث. سنسمي هذا الحدث TemperatureTropHaute. سيتم تغليف المعلومات المتعلقة بهذا الحدث في نوع TemperatureTropHauteEventArgs.
- تقوم فئة Souscripteur بالاشتراك في الحدث السابق. وعندما يتم إخطارها بالحدث، تعرض رسالة على وحدة التحكم.
- يقوم برنامج وحدة التحكم بإنشاء مرسل واثنين من المشتركين. يقوم بإدخال درجات الحرارة من لوحة المفاتيح وتسجيلها في مثيل Emetteur. إذا كانت درجة الحرارة مرتفعة جدًا، يقوم المثيل Emetteur بنشر الحدث TemperatureTropHaute.
للتوافق مع الطريقة الموصى بها لإدارة الأحداث، نقوم أولاً بتعريف النوع TemperatureTropHauteEventArgs لتغليف المعلومات المتعلقة بالحدث:
using System;
namespace Chap6 {
public class TemperatureTropHauteEventArgs:EventArgs {
// درجة الحرارة عند الحدث
public decimal Temperature { get; set; }
// الشركات المصنعة
public TemperatureTropHauteEventArgs() {
}
public TemperatureTropHauteEventArgs(decimal temperature) {
Temperature = temperature;
}
}
}
- السطر 6: المعلومات التي تغلفها الفئة TemperatureTropHauteEventArgs هي درجة الحرارة التي تسببت في الحدث TemperatureTropHaute.
الفئة Emetteur هي كما يلي:
using System;
namespace Chap6 {
public class Emetteur {
static decimal SEUIL = 19;
// درجة الحرارة الملحوظة
private decimal temperature;
// اسم المصدر
public string Nom { get; set; }
// تم الإبلاغ عن الحدث
public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
// قراءة / كتابة درجة الحرارة
public decimal Temperature {
get {
return temperature;
}
set {
temperature = value;
if (temperature > SEUIL) {
// يتم الإبلاغ عن الحدث للمشتركين
OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
}
}
}
// الإبلاغ عن حدث
protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
// إرسال الحدث TemperatureTropHaute إلى المشتركين
TemperatureTropHaute(this, evt);
}
}
}
- السطر 5: عتبة درجة الحرارة التي عند تجاوزها سيتم نشر الحدث TemperatureTropHaute.
- السطر 10: المرسل له اسم للتعريف به
- السطر 12: الحدث TemperatureTropHaute.
- الأسطر 15-26: الطريقة get التي تعرض درجة الحرارة والطريقة set التي تسجلها. الطريقة set هي التي تنشر الحدث TemperatureTropHaute إذا تجاوزت درجة الحرارة المراد تسجيلها عتبة السطر 5. وهي تنشر الحدث عبر الطريقة OnTemperatureTropHauteHandler في السطر 29، حيث تمرر لها كمعلمة كائن TemperatureTropHauteEventArgs الذي تم فيه تسجيل درجة الحرارة التي تجاوزت الحد.
- السطور 29-32: يتم نشر الحدث TemperatureTropHaute مع المرسل نفسه كمعلمة أولى والكائن TemperatureTropHauteEventArgs الذي تم استلامه كمعلمة ثانية.
الفئة Souscripteur التي ستشترك في الحدث TemperatureTropHaute هي التالية:
using System;
namespace Chap6 {
public class Souscripteur {
// الاسم
public string Nom { get; set; }
// مدير الحدث TemperatureTropHaute
public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
// عرض وحدة تحكم المشغل
Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signalé une température trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature);
}
}
}
- السطر 6: يتم تعريف كل مشترك باسم.
- الأسطر 9-12: الطريقة التي سيتم ربطها بالحدث TemperatureTropHaute. ولها توقيع من النوع delegate EventHandler<TEventArgs> الذي يجب أن يمتلكه مدير الأحداث. تعرض الطريقة على وحدة التحكم: اسم المشترك الذي يعرض الرسالة، واسم المرسل الذي أبلغ عن الحدث، ودرجة الحرارة التي أدت إلى حدوثه.
- لم يتم الاشتراك في الحدث TemperatureTropHaute لكائن Emetteur في الفئة Souscripteur. سيتم ذلك بواسطة فئة خارجية.
يربط البرنامج [Program.cs] جميع هذه العناصر ببعضها البعض:
using System;
namespace Chap6 {
class Program {
static void Main(string[] args) {
// إنشاء مرسل أحداث
Emetteur e1 = new Emetteur() { Nom = "e" };
// إنشاء لوحة من مشتركين
Souscripteur[] souscripteurs = new Souscripteur[2];
for (int i = 0; i < souscripteurs.Length; i++) {
// إنشاء مشترك
souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
// نقوم بتسجيله في الحدث TemperatureTropHaute من e1
e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
}
// قراءة درجات الحرارة من لوحة المفاتيح
decimal temperature;
Console.Write("Température (rien pour arrêter) : ");
string saisie = Console.ReadLine().Trim();
// طالما أن السطر الذي تم إدخاله غير فارغ
while (saisie != "") {
// هل الإدخال رقم عشري؟
if (decimal.TryParse(saisie, out temperature)) {
// درجة الحرارة صحيحة - يتم تسجيلها
e1.Temperature = temperature;
} else {
// يتم الإبلاغ عن الخطأ
Console.WriteLine("Température incorrecte");
}
// إدخال جديد
Console.Write("Température (rien pour arrêter) : ");
saisie = Console.ReadLine().Trim();
}//while
}
}
}
- السطر 6: إنشاء المرسل
- الأسطر 8-14: إنشاء مشتركين اثنين يتم اشتراكهما في حدث TemperatureTropHaute الخاص بالمُصدر.
- الأسطر 20-32: حلقة إدخال درجات الحرارة عبر لوحة المفاتيح
- السطر 24: إذا كانت درجة الحرارة المدخلة صحيحة، يتم إرسالها إلى الكائن Emetteur e1 الذي سيطلق الحدث TemperatureTropHaute إذا كانت درجة الحرارة أعلى من 19 درجة مئوية.
نتائج التنفيذ هي كما يلي:

