1. Giới thiệu
Hãy nhắc lại: sự kiện là một hợp đồng công khai, lời hứa của mã bạn rằng nó sẽ "gọi" những người đăng ký khi có điều gì đó quan trọng xảy ra. Trong các ví dụ ở bài trước bạn có thể đã thấy khai báo như sau:
public event Action<string> MessageSent;
Hoặc thậm chí:
public delegate void MyHandler(int value);
public event MyHandler SomethingHappened;
Cách này hoạt động, nhưng tiếp cận như vậy sinh ra cả một đống vấn đề — bắt đầu từ sự không đồng nhất về chữ ký phương thức đến việc không biết được ai phát ra sự kiện và chuyện gì đã xảy ra. Hãy tưởng tượng nếu mỗi lần nhấn phím trên bàn phím lại kích hoạt một hành vi ngẫu nhiên, mỗi lần — khác nhau trong cùng một chương trình với cùng một nhiệm vụ. Ví dụ, bạn đang chơi game và nhấn phím cách. Lúc trước đó nó là nhảy, nhưng giờ thì bất ngờ mở inventory. Thảm họa, đúng không?
Trong .NET có một "Hiến pháp" cho sự kiện — đó là quy ước về chữ ký handler và cấu trúc thông tin được truyền theo sự kiện. Dưới đây là phong cách chuẩn:
void Handler(object sender, EventArgs args);
Dòng này thấy quen chứ? Nó xuất hiện khắp nơi: từ click nút WinForms tới sự kiện hệ thống của ASP.NET và cả trong thư viện bên thứ ba.
2. EventHandler và EventArgs là gì
Ý tưởng chính: mỗi sự kiện truyền báo hai thứ:
- Ai đã gây ra sự kiện? sender
- Điều gì đã xảy ra? EventArgs — dữ liệu bổ sung
Trong thế giới C# được trình bày như sau:
public delegate void EventHandler(object sender, EventArgs e);
- object sender — tham chiếu tới người khởi xướng sự kiện. Nó có thể là bất cứ thứ gì — thường là this.
- EventArgs e — đối tượng chứa thông tin bổ sung về sự kiện. Với các kịch bản đơn giản dùng EventArgs.Empty, với những kịch bản phức tạp hơn thì tạo lớp kế thừa EventArgs riêng.
Sự thật thú vị
Trong khuyến nghị chính thức của .NET cho API công khai, người ta chấp nhận rằng sự kiện nên có chữ ký (object sender, EventArgs e). Nếu bạn thấy sự kiện thiếu sender và EventArgs — nhiều khả năng đó là phiên bản tác giả tự giản lược, chứ không phải phong cách .NET chuẩn.
Lợi ích lớn của một chuẩn duy nhất
Với một chuẩn thống nhất, việc log, test, đăng ký chung và mở rộng hệ thống dễ dàng hơn. Mở mã nguồn WinForms, WPF, ASP.NET — bạn sẽ thấy cùng một mẫu lặp lại.
3. Cách dùng mẫu sự kiện chuẩn
1. Dùng delegate tích hợp sẵn EventHandler
Thay vì khai báo delegate riêng, bạn có thể dùng sẵn:
public event EventHandler SmthHappened;
Và giờ handler luôn có dạng giống nhau:
private void OnSmthHappened(object sender, EventArgs e)
{
// Logic phản ứng với sự kiện
}
Đăng ký vẫn đơn giản:
myObj.SmthHappened += OnSmthHappened;
Quan trọng: nếu sự kiện không truyền dữ liệu bổ sung, hãy dùng EventArgs.Empty.
2. Tạo đối số sự kiện riêng
Nếu cần truyền thông tin (kết quả tính toán, tên file, lỗi), tạo lớp kế thừa từ EventArgs:
public class CalculationEventArgs : EventArgs
{
public double Result { get; }
public CalculationEventArgs(double result) => Result = result;
}
Sau đó dùng delegate generic chuẩn — EventHandler<TEventArgs>:
public event EventHandler<CalculationEventArgs> CalculationFinished;
Và handler giờ nhận đúng kiểu đối số của bạn:
private void OnCalculationFinished(object sender, CalculationEventArgs e)
{
Console.WriteLine($"Tính toán hoàn tất. Kết quả: {e.Result}");
}
4. Ví dụ ứng dụng
Phát triển dự án học tập của chúng ta — giả sử có một máy tính (calculator) cộng hoặc trừ số và báo khi hoàn thành phép tính qua sự kiện.
Ví dụ tối giản
public class Calculator
{
public event EventHandler<CalculationEventArgs> CalculationPerformed;
public void Add(int a, int b)
{
int result = a + b;
// "Bắn" sự kiện
CalculationPerformed?.Invoke(this, new CalculationEventArgs(result));
}
}
public class CalculationEventArgs : EventArgs
{
public int Result { get; }
public CalculationEventArgs(int result) => Result = result;
}
Đăng ký và sử dụng:
var calc = new Calculator();
calc.CalculationPerformed += (sender, e) =>
{
Console.WriteLine($"Kết quả của phép toán: {e.Result}");
};
calc.Add(10, 20);
// In ra: Kết quả của phép toán: 30
Đó là toàn bộ "chuẩn": handler luôn nhận đối tượng gửi và đối tượng đối số — tổng quát và dễ hiểu.
5. Những chi tiết hữu ích
Ẩn logic gọi sự kiện: phong cách tốt
Trong .NET người ta thường tách logic gọi sự kiện vào một phương thức protected với tiền tố On:
protected virtual void OnCalculationPerformed(CalculationEventArgs e)
{
CalculationPerformed?.Invoke(this, e);
}
Và bên trong các phương thức như "Add", "Subtract" chỉ gọi phương thức đó:
public void Add(int a, int b) => OnCalculationPerformed(new CalculationEventArgs(a + b));
Phong cách này cho phép lớp con override hành vi gọi sự kiện và giảm khả năng quên gọi sự kiện.
Sơ đồ: Cấu trúc sự kiện chuẩn .NET
graph LR
A[Đối tượng-publisher] -- "event EventHandler/ EventHandler<TEventArgs>" --> B[Danh sách các subscriber]
B -- "Method handler (object sender, EventArgs e)" --> C[Phản ứng khi có sự kiện]
A -- "this (sender)" --> C
A -- "EventArgs (dữ liệu)" --> C
So sánh cách khai báo sự kiện
| Phương pháp | Truyền người khởi xướng (sender) | Truyền đối số | Tính tổng quát | Sử dụng trong .NET |
|---|---|---|---|---|
|
Không | Có | Thấp | Không |
|
Có | Có | Trung bình | Hiếm (không phổ biến) |
|
Có | Không (EventArgs) | Cao | Có, chuẩn |
|
Có | Có (MyArgs) | Rất cao | Có, chuẩn |
Giá trị thực tiễn trong phỏng vấn và code thực tế
Dùng mẫu sự kiện chuẩn là must-have cho .NET developer. Trong phỏng vấn họ rất có thể sẽ hỏi về EventHandler và cặp sender/EventArgs. Sự kiện kiểu Action<T> thường bị coi là "phiên bản giản lược".
Trong dự án thực tế, cách này làm cho hợp tác, testing, mở rộng và bảo trì dễ hơn. Các thư viện bên thứ ba (logging, profiler) dễ tích hợp khi dùng form chuẩn.
Sơ đồ khối gọi sự kiện
flowchart TD
subgraph A[Class-publisher]
C1((Đối tượng))
C2[Phương thức gọi sự kiện]
C3["event EventHandler<MyArgs>"]
end
subgraph B[Class-subscriber]
D1((Đăng ký))
D2["Handler sự kiện (object sender, MyArgs args)"]
end
C1 -- Gọi --> C2
C2 -- "Invoke(this, args)" --> C3
C3 -- "Thông báo" --> D1
D1 -- "Gọi" --> D2
6. Mẹo, chi tiết và lỗi thường gặp
1. Chữ ký handler. Muốn làm sự kiện kiểu Action<int>? Cám dỗ là có thật, nhưng bạn sẽ mất sender và tính tương thích với hệ sinh thái.
2. Truyền đối số. Đừng nhầm EventArgs với tham số bình thường. Tất cả dữ liệu cho handler nên đặt trong đối tượng args.
3. Dùng null cho args. Thay vì null hãy dùng EventArgs.Empty nếu không có dữ liệu bổ sung.
4. Typing yếu. Đừng làm một sự kiện "ăn được mọi thứ" bằng EventHandler và nhét đủ thứ vào đó. Tạo lớp args riêng cho mỗi sự kiện — readability và reliability tốt hơn.
5. Lỗi khi gọi sự kiện. Luôn kiểm tra có subscriber: SomeEvent?.Invoke(this, e). Nếu không có subscriber, tham chiếu sự kiện là null.
6. Vi phạm đóng gói. Đừng gọi sự kiện từ bên ngoài class-publisher. Sự kiện chỉ để đăng ký/hủy đăng ký; việc gọi nên làm nội bộ thông qua phương thức On....
GO TO FULL VERSION