C# cơ bản .NET Core
Lập trình C# Cơ bản

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 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 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 EventHandler event_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


Đăng ký nhận bài viết mới