Event trong C#
Các sự kiện (Event) là cơ chế để một đối tượng (đối tượng của lớp) này thông báo đến
đối tượng khác có điều gì đó mà lớp khác quan tâm vừa xảy ra.
Lớp mà từ đó gửi đi sự kiện gọi tên nó là publisher
và các lớp nhận được sự kiện gọi là là các subsriber
.
Để làm được việc này nó hoạt động giống hệt cơ chế delegate
,
thực tế là trong .NET các Event xây dựng với nền tảng chính là delegate,
nên trước khi tìm hiểu Event hay vào tìm hiểu
delegate trước.
Vậy có vấn đề gì khi sử dụng delegate mà phải thêm khái niệm Event, hãy tìm hiểu trường hợp sau
Vấn đề của delegate và sự giải quyết của Event
Ví dụ dưới đây sẽ dùng delegate
(đã biết ở phần trước) để xây dựng cơ chế để một lớp này đăng ký nhận sự kiện từ một lớp khác
DelegateEvent.cs
using System; namespace CS009_Event { /* Publisher là lớp phát đi sự kiện, bằng cách gọi một deleage trong phương thức Send */ public class Publisher { public delegate void NotifyNews (object data); public NotifyNews event_news; public void Send () { event_news?.Invoke ("Co tin moi"); } } // SubscriberA lớp này đăng ký nhận sự kiện từ Publisher, // bằng phương thức Sub, khi sự kiện xảy ra nó sẽ gọi ReceiverFromPublisher public class SubscriberA { public void Sub (Publisher p) { p.event_news += ReceiverFromPublisher; } void ReceiverFromPublisher (object data) { Console.WriteLine ("SubscriberA: " + data.ToString ()); } } // SubscriberA lớp này đăng ký nhận sự kiện từ Publisher, // bằng phương thức Sub - khi đăng ký nó hủy việc nhận sự kiện của các đối tượng khác, // khi sự kiện xảy ra nó sẽ gọi ReceiverFromPublisher public class SubscriberB { public void Sub (Publisher p) { p.event_news = null; // Hủy các đối tượng khác nhận sự kiện p.event_news += ReceiverFromPublisher; } void ReceiverFromPublisher (object data) { Console.WriteLine ("SubscriberB: " + data.ToString ()); } } }
Khi áp dụng trong lớp Program
static void TestDelegate() { Publisher p = new Publisher(); SubscriberA sa = new SubscriberA(); SubscriberB sb = new SubscriberB(); sa.Sub(p); sb.Sub(p); p.Send(); }
Khi chạy thử - gọi hàm TestDelegate
, thì kết quả in ra
SubscriberB: Co tin moi
Phân tích vấn đề của đoạn mã trên:
Lớp Publisher
xây dựng một delegate có tên NotifyNews
và khai báo thuộc tính event_news
triển khai nó, khi Publisher thi hành Send()
nó sẽ thi hành delegate này và như vậy những đối tượng nào đăng ký vào delegate
sẽ có cơ hội nhận thông tin mới từ Publisher
Hai lớp SubscriberA
và SubscriberB
tiến hành đăng ký phương thức ReceiverFromPublisher
vào Delegate của Pushisher, và như vậy khi chạy code đã có kết quả như trên.
Tuy nhiên, nhìn vào phương thức public void Sub(Publisher p)
của SubscriberB
thì đoạn mã:
p.event_news = null;
Nó đã gán event_news bằng null, có nghĩa là việc đăng ký của SubcriberA
lúc trước bị loại bỏ bởi SubcriberB
, dẫn tới chỉ có SubcriberB
nhận được tin mới. Điều này là phá hỏng nguyên tắc hoạt động của mô hình lập trình sự kiện - phá vỡ sự đóng gói
Để giải quyết vấn đề trên, thật đơn giản với .NET chỉ cần thêm từ khóa event
vào định nghĩa event_news
của Pushliser, và từ đây event_news
gọi là Event chứ không còn gọi là Delegate
public event NotifyNews event_news;
Từ lúc này, các Subscriber chỉ có thể đăng ký nhận sự kiện với toán tử +=
hoặc hủy nhận sự kiện với toán tử -=
chứ không thể thực hiện gán p.event_news = null
vì nếu viết code như vậy lập tức báo lỗi.
Tóm lại, Event là Delegate nhưng khi khai báo thêm từ khóa event
, dẫn tới chỉ có thể thực hiện toán tử +=
hoặc -=
với Event
Event trong thư viện .NET
Các Event ví dụ như KeyDown
, GotFocus
, Load
của Form,
Application.ApplicationExit
, Application.Idle
...
đều xây dựng từ một delegate có tên EventHandler
, nó đã định nghĩa sẵn có trong thư viện .NET với dạng:
public delegate void EventHandler(object sender?, EventArgs e);
public delegate void EventHandler<TEventArgs>(object sender?, TEventArgs e);
Như vậy bạn có thể sử dụng luôn delegate
EventHandler để xây dựng các Event của riêng mình sử dụng cho các Publisher,
chỉ cần xây dựng các lớp phái sinh từ EventArgs
với mục đích thêm vào các tham số riêng khi gửi sử kiện.
Ví dụ:
UseEventHandler.cs
using System; namespace CS009 { // Xây dựng lớp MyEventArgs kế thừa từ EventArgs public class MyEventArgs : EventArgs { public MyEventArgs (string data) { this.data = data; } // Lưu dữ liệu gửi đi từ publisher private string data; public string Data { get { return data; } } } // Xây dựng lớp, phát đi sự kiện (data) public class ClassA { // Tạo Event với EventHandler public event EventHandlerevent_news; public void Send () { event_news?.Invoke (this, new MyEventArgs ("Có tin mới Abc ...")); } } public class ClassB { public void Sub (ClassA p) { p.event_news += ReceiverFromPublisher; } private void ReceiverFromPublisher (object sender, MyEventArgs e) { Console.WriteLine ("ClassB: " + e.Data); } } public class ClassC { public void Sub (ClassA p) { p.event_news += ReceiverFromPublisher; } private void ReceiverFromPublisher (object sender, MyEventArgs e) { Console.WriteLine ("ClassC: " + e.Data); } } }
Khi sử dụng
static void TestEventHandler () { ClassA p = new ClassA(); ClassB sa = new ClassB(); ClassC sb = new ClassC(); sa.Sub (p); // sa đăng ký nhận sự kiện từ p sb.Sub (p); // sb đăng ký nhận sự kiện từ p p.Send (); }
Kết quả chạy
ClassB: Có tin mới Abc ... ClassC: Có tin mới Abc ...
Lưu ý: lớp MyEventArgs
xây dựng kế thừa từ EventArgs
ở trên
với mục đích chuyên trở thêm dữ liệu,
để đảm bảo dữ liệu không bị sửa đổi bởi các Subsriber
thì data
xây dựng theo cách thức như trên để chỉ có thể đọc! (sau khi khởi tạo từ hàm khởi tạo)
Source code: CS009_Event (Git), hoặc tải cs009-event