Event trong C#

Các sự kiện (Event) là cơ chế để một lớp (đối tượng của lớp) này thông báo đến lớp khác có điều gì đó mà lớp khác quan tâm 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, và 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

public class Publisher
{
    public delegate void NotifyNews(object data);

    public NotifyNews event_news;

    public void Send()
    {
        event_news?.Invoke("Co tin moi");
    }
}

public class SubscriberA
{
    public void Sub(Publisher p)
    {
        p.event_news += ReceiverFromPublisher;
    }

    void ReceiverFromPublisher(object data)
    {
        Console.WriteLine("SubscriberA: " + data.ToString());
    }
}
public class SubscriberB
{
    public void Sub(Publisher p)
    {
        p.event_news = null;
        p.event_news += ReceiverFromPublisher;
    }

    void ReceiverFromPublisher(object data)
    {
        Console.WriteLine("SubscriberB: "+ data.ToString());
    }
}


class Program
{
    static void Main(string[] args)
    {
        Publisher p = new Publisher();
        SubscriberA sa = new SubscriberA();
        SubscriberB sb = new SubscriberB();

        sa.Sub(p);
        sb.Sub(p);

        p.Send();

    }
}

Chạy đoạn mã trên, kết quả là:

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 SubscriberASubscriberB 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 SubscriberBthì đ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 là EventHandler

public delegate void EventHandler(object sender, EventArgs e);
public delegate void EventHandler(object sender, TEventArgs e)
        where TEventArgs : EventArgs;

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ụ:

public class MyEventArgs : EventArgs
{
    public MyEventArgs(string data)
    {
        this.data = data;
    }

    private string data;

    public string Data {
        get { return data; }
    }
}

public class Publisher
{
    //Tạo Event với EventHandler
    public event EventHandler<MyEventArgs> event_news;
    public void Send()
    {
        event_news?.Invoke(this, new MyEventArgs("Có tin mới"));
    }
}


public class SubscriberA
{
    public void Sub(Publisher p)
    {
            p.event_news += ReceiverFromPublisher;
    }

    private void ReceiverFromPublisher(object sender, MyEventArgs e)
    {
        Console.WriteLine("SubscriberA: " + e.Data);
    }
}
public class SubscriberB
{
    public void Sub(Publisher p)
    {
        p.event_news += ReceiverFromPublisher;
    }

    private void ReceiverFromPublisher(object sender, MyEventArgs e)
    {
        Console.WriteLine("SubscriberB: " + e.Data);
    }
}

class Program
{
    static void Main(string[] args)
    {
        Publisher p = new Publisher();
        SubscriberA sa = new SubscriberA();
        SubscriberB sb = new SubscriberB();

        sa.Sub(p);
        sb.Sub(p);

        p.Send();

    }
}

Kết quả chạy

SubscriberA: Có tin mới
SubscriberB: Có tin mới

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)