Skip to content

8. أحداث المستخدم

في الفصل السابق، ناقشنا مفهوم الأحداث المرتبطة بمكونات النماذج. والآن سنلقي نظرة على كيفية إنشاء الأحداث في فئاتنا الخاصة.

8.1. كائنات التفويض المحددة مسبقًا

لقد تناولنا مفهوم كائن الوكيل في الفصل السابق، ولكننا لم نتعمق فيه في ذلك الوقت. عندما نظرنا في كيفية إعلان معالجات الأحداث لمكونات النموذج، صادفنا كودًا مشابهًا لما يلي:


            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 على النحو التالي:

 

يحدد مندوب Visit 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);

وبالتالي يمكننا كتابة:

EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

المتغير من نوع delegate هو في الواقع قائمة من المراجع إلى الطرق المطابقة للـ delegate. لإضافة طريقة جديدة M إلى المتغير evtHandler أعلاه، نستخدم الصيغة:

evtHandler+=new EvtHandler(M);

يمكن استخدام الرمز += حتى لو كان evtHandler قائمة فارغة.

التعليمات:


            this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

يضيف EventHandler إلى قائمة أساليب الحدث buttonAfficher.Click. عندما يحدث الحدث Click على المكون buttonAfficher، يقوم VB بتنفيذ:

            buttonAfficher.Click(source, evt);

حيث:

  • source هو الكائن وراء الحدث
  • evt من نوع EventArgs ولا يحتوي على أي معلومات

جميع طرق التوقيع void M(object,EventArgs) المرتبطة بالحدث Click بواسطة:


            this.buttonAfficher.Click += new System.EventHandler(M);

سيتم استدعاؤها بالمعلمات (source, evt) التي تم إرسالها بواسطة VB.

8.2. تعريف كائنات المندوب

التعليمات


    public delegate int Opération(int n1, int n2);

يحدد نوعًا يسمى Operation يمثل نموذجًا أوليًا لدالة تقبل عددين صحيحين وتُرجع عددًا صحيحًا واحدًا. الكلمة الرئيسية delegate هي التي تجعل من Operation تعريفًا لنموذج أولي لدالة.

ستسجل المتغير op من النوع Operation قائمة بالدوال المطابقة للنموذج الأولي Operation :

int f1(int,int)
int f2(int,int)
...
int fn(int,int)

يتم تسجيل طريقة fi في المتغير op بواسطة op=new Operation(fi) أو ببساطة op=fi. لإضافة طريقة fj إلى قائمة الدوال المسجلة، نكتب op+= fj. لإزالة طريقة fk نكتب op-=fk. إذا كتبنا في مثالنا n=op(n1,n2)، فسيتم تنفيذ مجموعة الطرق المخزنة في op بالمعلمات n1 و n2. وستكون النتيجة n المستردة هي نتيجة آخر طريقة تم تنفيذها. لا يمكن الحصول على النتائج التي تنتجها جميع الطرق. ولهذا السبب، إذا قمت بتخزين قائمة بالطرق في دالة مفوضة، فستُرجع عادةً نتيجة من النوع void.

انظر المثال التالي:


using System;
namespace Chap6 {
    class Class1 {
         // function prototype definition
         // accepts 2 integers as parameters and returns an integer
        public delegate int Opération(int n1, int n2);
 
         // two instance methods corresponding to the prototype
        public int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
         }//add
 
        public int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
         }//subtract
 
         // a static method corresponding to the prototype
        public static int Augmenter(int n1, int n2) {
            Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")");
            return n1 + 2 * n2;
         }//increase
 
        static void Main(string[] args) {
 
             // define an operation object to store functions
             // we register the static function increase
            Opération op = Augmenter;
             // the delegate is executed
            int n = op(4, 7);
            Console.WriteLine("n=" + n);
 
             // creation of a c1 object of type class1
            Class1 c1 = new Class1();
             // we register c1's add method in the delegate
            op = c1.Ajouter;
             // execution of delegated object
            n = op(2, 3);
            Console.WriteLine("n=" + n);
             // the subtract method of c1 is registered in the delegate
            op = c1.Soustraire;
            n = op(2, 3);
            Console.WriteLine("n=" + n);
             //registration of two functions in the delegate
            op = c1.Ajouter;
            op += c1.Soustraire;
             // execution of delegated object
            op(0, 0);
             // remove a function from the delegate
            op -= c1.Soustraire;
             // the delegate is executed
            op(1, 1);
        }
    }
}
  • السطر 3: يحدد فئة Class1.
  • السطر 6: تعريف الوكيل Operation: نموذج أولي للطرق التي تقبل معلمتين من النوع int وتُرجع قيمة من النوع int
  • الأسطر 9-12: طريقة المثيل Add إلى توقيع الوكيل Operation.
  • الأسطر 14-17: طريقة المثيل S subtract إلى توقيع الوكيل Operation.
  • الأسطر 20-23: طريقة class Ancrease إلى توقيع الوكيل Operation.
  • السطر 25: تم تنفيذ الدالة Main
  • السطر 20: المتغير op من نوع delegate Operation. وسيحتوي على قائمة بالطرق ذات توقيع النوع delegate Operation. ويتم تعيين مرجع الطريقة الأولى له، وهو مرجع الطريقة الثابتة Class1.Augmenter.
  • السطر 31: يتم تنفيذ الوكيل op: جميع الطرق المشار إليها بواسطة op والتي سيتم تنفيذها. سيتم تنفيذها باستخدام المعلمات التي تم تمريرها إلى الوكيل op. هنا، سيتم تنفيذ الطريقة الثابتة Class1.Augmenter فقط.
  • السطر 35: يتم إنشاء مثيل c1 لفئة Class1.
  • السطر 37: يتم تعيين الطريقة c1.Ajouter للمثيل إلى الوكيل op. كانت Increase طريقة ثابتة، أما Add فهي طريقة مثيل. أردنا إظهار أنه لا يهم.
  • السطر 39: يتم تنفيذ الوكيل op: سيتم تنفيذ Add باستخدام المعلمات التي تم تمريرها إلى الوكيل op.
  • السطر 42: نفعل الشيء نفسه مع الطريقة الخاصة بالمثيل Subtract.
  • السطران 46-47: الطريقتان Add و Subtract في المندوب op.
  • السطر 49: يتم تنفيذ المندوب op: سيتم تنفيذ كل من Add و Subtract باستخدام المعلمات التي تم تمريرها إلى المندوب op.
  • السطر 51: تتم إزالة الطريقة Subtract من العملية المفوضة.
  • السطر 53: يتم تنفيذ عملية التفويض: سيتم تنفيذ Add المتبقية.

النتائج هي كما يلي:

1
2
3
4
5
6
7
8
9
Augmenter(4,7)
n=18
Ajouter(2,3)
n=5
Soustraire(2,3)
n=-1
Ajouter(0,0)
Soustraire(0,0)
Ajouter(1,1)

8.3. المندوبون أم الواجهات؟

قد تبدو مفاهيم الوكلاء والواجهات متشابهة إلى حد ما، وقد نتساءل عن الفروق الدقيقة بين هذين المفهومين. لنأخذ المثال التالي، الذي يشبه أحد الأمثلة التي درسناها سابقًا:


using System;
namespace Chap6 {
    class Program1 {
         // function prototype definition
         // accepts 2 integers as parameters and returns an integer
        public delegate int Opération(int n1, int n2);
 
         // two instance methods corresponding to the prototype
        public static int Ajouter(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
         }//add
 
        public static int Soustraire(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
         }//subtract
 
         // Executing a delegate
        public static int Execute(Opération op, int n1, int n2){
            return op(n1, n2);
        }
 
        static void Main(string[] args) {
             // delegate execution Add
            Console.WriteLine(Execute(Ajouter, 2, 3));
             // delegate execution Subtract
            Console.WriteLine(Execute(Soustraire, 2, 3));
             // executing a multicast delegate
            Opération op = Ajouter;
            op += Soustraire;
            Console.WriteLine(Execute(op, 2, 3));
             // remove a function from the delegate
            op -= Soustraire;
             // the delegate is executed
            Console.WriteLine(Execute(op, 2, 3));
        }
    }
}

السطر 20، تتوقع الطريقة Execute مرجعًا إلى كائن من نوع مندوب Operation المُعرَّف في السطر 6. وهذا يؤدي إلى التبديل إلى طرق Execute المختلفة (السطور 26 و28 و32 و36). ويمكن أيضًا تحقيق خاصية تعدد الأشكال هذه باستخدام :


using System;
 
namespace Chap6 {
 
     // interface IOperation
    public interface IOperation {
        int operation(int n1, int n2);
    }
 
     // class Add
    public class Ajouter : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")");
            return n1 + n2;
        }
    }
 
     // class Subtract
    public class Soustraire : IOperation {
        public int operation(int n1, int n2) {
            Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")");
            return n1 - n2;
        }
    }
 
     // test class
    public static class Program2 {
         // Executing the single method of the IOperation interface
        public static int Execute(IOperation op, int n1, int n2) {
            return op.operation(n1, n2);
        }
 
        public static void Main() {
             // delegate execution Add
            Console.WriteLine(Execute(new Ajouter(), 2, 3));
             // delegate execution Subtract
            Console.WriteLine(Execute(new Soustraire(), 2, 3));
        }
    }
}
  • الأسطر 6-8: تحدد واجهة [IOperation] طريقة operation.
  • الأسطر 11-16 و19-24: تنفذ فئتا [Add] و[Subtract] واجهة [IOperation].
  • الأسطر 29-31: الطريقة Execute التي يكون المعامل الأول لها من نوع واجهة IOperation. ستتلقى الطريقة Execute بالتتابع، كمعامل أول، مثيلًا للفئة Add ثم مثيلًا للفئة Subtract.

الجانب المتعدد الأشكال للمندوب في المثال السابق. وفي الوقت نفسه، يوضح كلا المثالين الاختلافات بين هذين المفهومين.

يمكن استبدال نوعي المندوب والواجهة

  • إذا كانت الواجهة تحتوي على طريقة واحدة فقط. في الواقع، المندوب هو غلاف لطريقة واحدة، في حين أن الواجهة يمكنها تعريف عدة طرق.
  • إذا لم يتم استخدام الجانب المتعدد البث للمندوب. لا يوجد مفهوم البث المتعدد في الواجهة.

إذا تم استيفاء هذين الشرطين، فيمكننا الاختيار بين التوقيعين التاليين للطريقة Execute :


int Execute(IOperation op, int n1, int n2)
int Execute(Opération op, int n1, int n2)

أما الطريقة الثانية، التي تستخدم الوكيل، فيمكن استخدامها بمرونة أكبر. في التوقيع الأول، يجب أن تنفذ المعلمة الأولى للطريقة واجهة IOperation. وهذا يتطلب إنشاء فئة لتعريف الطريقة التي سيتم تمريرها كمعلمة أولى إلى Execute. أما في التوقيع الثاني، فأي طريقة موجودة ذات التوقيع الصحيح ستفي بالغرض. ولا يلزم إنشاء أي بنية إضافية.

8.4. إدارة الأحداث

يمكن استخدام فئات تفويض الكائنات لتعريف الأحداث. يمكن لفئة C1 تعريف حدث evt على النحو التالي:

  • يتم تعريف نوع التفويض داخل أو خارج الفئة C1:
delegate TResult Evt(T1 param1, T2 param2, ...);
  • تُعرّف الفئة C1 مندوبًا Evt :
public Evt Evt1;
  • عندما ترغب مثيل الفئة C1 في الإبلاغ عن حدث، فإنها ستنفذ مندوبها Evt1 ممررة له المعلمات المحددة بواسطة المندوب Evt. سيتم بعد ذلك تنفيذ جميع الطرق المسجلة في المندوب Evt1 باستخدام هذه المعلمات. يمكننا القول بأنها قد تم إخطارها بالحدث Evt1.
  • إذا أراد كائن c2 يستخدم c1 أن يتم إخطاره عند حدوث حدث Evt1 على الكائن c1، فسيقوم بتسجيل أحد c2.M الخاص به في الكائن المفوض c1.Evt1 للكائن c1. سيتم تنفيذ c2.M الخاص به في كل مرة يحدث فيها الحدث Evt1 على الكائن c1. يمكنهم أيضًا إلغاء الاشتراك إذا لم يعودوا يرغبون في تلقي إخطارات بالحدث.
  • نظرًا لأن الكائن المفوض c1.Evt1 يمكنه تسجيل عدة طرق، يمكن لـ ci مختلفة التسجيل مع c1.Evt1 لتلقي إشعار بالحدث Evt1 على c1.

في هذا السيناريو، لدينا:

  • فئة تشير إلى حدث
  • فئات يتم إخطارها بهذا الحدث. ويقال إنها تشترك في الحدث.
  • نوع مفوض يحدد توقيع الطرق التي سيتم إخطارها بالحدث

يحدد إطار عمل .NET ما يلي:

  • توقيع قياسي للمندوب حدث
public delegate void MyEventHandler(object source, EventArgs evtInfo);
  • source : الكائن الذي أبلغ عن الحدث
  • evtInfo: كائن من نوع EventArgs أو مشتق منه يوفر معلومات حول الحدث
  • يجب أن ينتهي اسم المندوب بـ EventHandler
  • طريقة قياسية لإعلان MyEventHandler في فئة:
1
2
3
4
public Class C1{
    public event MyEventHandler Evt1;
...
}

نوع Evt1 هو مندوب. توجد الكلمة الرئيسية event لتقييد العمليات التي يمكن إجراؤها عليه:

  • من خارج الفصل C1، لا يمكن إجراء سوى عمليات += و -=. وهذا يمنع حذف الطرق المشتركة في الحدث (بسبب خطأ المطور، على سبيل المثال). يمكنك ببساطة الاشتراك (+=) أو إلغاء الاشتراك (-=) في الحدث.
  • لا يمكن إلا لمثيل من النوع C1 تنفيذ الاستدعاء Evt1(source,evtInfo) الذي يؤدي إلى تشغيل الطرق المشتركة في Evt1.

يوفر إطار عمل .NET طريقة عامة تفي بتوقيع مندوب الحدث:

public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs
  • يستخدم الوكيل EventHandler النوع العام TEventArgs وهو نوع المعلمة الثانية
  • يجب أن يكون النوع TEventArgs مشتقًا من EventsArgs (حيث TEventArgs : EventArgs)

مع هذا المندوب العام، سيتبع إعلان الحدث X في الفئة C المخطط الموصى به التالي:

  • تحديد نوع XEventArgs مشتق من EventArgs لتغليف معلومات الحدث X
  • تحديد في الفئة C EventHandler<XEventArgs>.
  • تحديد في الفئة C طريقة محمية
protected void OnXHandler(XEventArgs e);

لـ"نشر" الحدث X للمشتركين.

انظر المثال التالي:

  • تغلف الفئة Transmitter درجة حرارة. يتم رصد هذه الدرجة. عندما تتجاوز هذه الدرجة عتبة معينة، يتم تشغيل حدث. سنسمي هذا الحدث TemperatureTropHaute. سيتم تغليف المعلومات المتعلقة بهذا الحدث في نوع TemperatureTropHauteEventArgs.
  • تقوم فئة Underwriter بالاشتراك في الحدث السابق. عند إخطارها بالحدث، تعرض رسالة على وحدة التحكم.
  • يقوم برنامج وحدة التحكم بإنشاء جهاز إرسال واثنين من المشتركين. يقوم بإدخال درجات الحرارة من لوحة المفاتيح وتخزينها في Transmitter. إذا كانت درجة الحرارة مرتفعة جدًا، يقوم Transmitter بنشر الحدث TemperatureTropHaute.

للتوافق مع الطريقة الموصى بها لإدارة الأحداث، نحدد أولاً النوع TemperatureTropHauteEventArgs لتغليف معلومات الحدث:


using System;
 
namespace Chap6 {
    public class TemperatureTropHauteEventArgs:EventArgs {
         // temperature during evt
        public decimal Temperature { get; set; }
         // manufacturers
        public TemperatureTropHauteEventArgs() {
        }
        public TemperatureTropHauteEventArgs(decimal temperature) {
            Temperature = temperature;
        }
    }
}
  • السطر 6: المعلومات التي تحتوي عليها TemperatureTropHauteEventArgs هي درجة الحرارة التي تسببت في الحدث TemperatureTropHaute.

الفئة Transmitter هي كما يلي:


using System;
 
namespace Chap6 {
    public class Emetteur {
        static decimal SEUIL = 19;
 
         // observed temperature
        private decimal temperature;
         // name of source
        public string Nom { get; set; }
         // event reported
        public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute;
 
         // read/write temperature
        public decimal Temperature {
            get {
                return temperature;
            }
            set {
                temperature = value;
                if (temperature > SEUIL) {
                     // subscribers are notified of the event
                    OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature));
                }
            }
        }
 
         // reporting an event
        protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) {
             // issue of event TemperatureTropHaute to subscribers
            TemperatureTropHaute(this, evt);
        }
    }
}
  • السطر 5: عتبة درجة الحرارة التي عند تجاوزها سيتم نشر الحدث TemperatureTropHaute.
  • السطر 10: المرسل له اسم لأغراض التعريف
  • السطر 12: الحدث TemperatureTropHaute.
  • الأسطر 15-26: الطريقة get التي تحدد درجة الحرارة والطريقة set التي تسجلها. هذه هي المجموعة التي تنشر الحدث TemperatureTropHaute إذا تجاوزت درجة الحرارة المراد تسجيلها الحد المحدد في السطر 5. وهي تنشر الحدث باستخدام OnTemperatureTropHauteHandler من السطر 29 عن طريق تمرير TemperatureTropHauteEventArgs الذي تم فيه تسجيل درجة الحرارة التي تجاوزت الحد المحدد.
  • الأسطر 29-32: يتم نشر الحدث TemperatureTropHaute مع المرسل نفسه كمعلمة أولى والكائن TemperatureTropHauteEventArgs الذي تم استلامه كمعلمة.

الفئة Underwriter التي ستشترك في الحدث TemperatureTropHaute هي كما يلي:


using System;
 
namespace Chap6 {
    public class Souscripteur {
         // name
        public string Nom { get; set; }
 
         // event manager TemperatureTropHaute
        public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) {
             // operator console display
            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. وهي تحمل توقيع النوع المندوب EventHandler<TEventArgs> الذي يجب أن يتوفر فيه مدير الأحداث. تعرض هذه الطريقة على وحدة التحكم: اسم المشترك الذي يعرض الرسالة، واسم المرسل الذي أبلغ عن الحدث، ودرجة الحرارة التي أدت إلى حدوث الحدث.
  • لا يتم الاشتراك في الحدث TemperatureTropHaute لكائن Transmitter في الفصل Underwriter. سيتم ذلك بواسطة فصل خارجي.

يربط البرنامج [Program.cs] جميع هذه العناصر معًا:


using System;
namespace Chap6 {
    class Program {
        static void Main(string[] args) {
             // creation of an evts transmitter
            Emetteur e1 = new Emetteur() { Nom = "e" };
             // creation of a table of 2 subscribers
            Souscripteur[] souscripteurs = new Souscripteur[2];
            for (int i = 0; i < souscripteurs.Length; i++) {
                 // creation subscriber
                souscripteurs[i] = new Souscripteur() { Nom = "s" + i };
                 // we subscribe him to e1's TemperatureTropHaute event
                e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute;
            }
             // temperatures are read from the keyboard
            decimal temperature;
            Console.Write("Température (rien pour arrêter) : ");
            string saisie = Console.ReadLine().Trim();
             // as long as the line entered is not empty
            while (saisie != "") {
                 // is the input a decimal number?
                if (decimal.TryParse(saisie, out temperature)) {
                     // correct temperature - recorded
                    e1.Temperature = temperature;
                } else {
                    // on signale l'erreur
                    Console.WriteLine("Température incorrecte");
                }
                 // new entry
                Console.Write("Température (rien pour arrêter) : ");
                saisie = Console.ReadLine().Trim();
             }//while
        }
    }
}
  • السطر 6: إنشاء جهاز الإرسال
  • الأسطر 8-14: إنشاء مشتركين اثنين في حدث TemperatureTropHaute الخاص بجهاز الإرسال.
  • الأسطر 20-32: حلقة إدخال درجة الحرارة من لوحة المفاتيح
  • السطر 24: إذا كانت درجة الحرارة المدخلة صحيحة، يتم إرسالها إلى الكائن Transmitter e1 الذي سيطلق TemperatureTropHaute إذا كانت درجة الحرارة أعلى من 19 درجة مئوية.

والنتائج هي كما يلي:

1
2
3
4
Température (rien pour arrêter) : 17
Température (rien pour arrêter) : 21
Souscripteur [s0] : la source [e] a signalé une température trop haute : [21]
Souscripteur [s1] : la source [e] a signalé une température trop haute : [21]