1. Введение
Người mới thường hỏi: "Sự kiện được dùng ở đâu trong thực tế? Liệu mọi tương tác giữa các class đều dựa trên sự kiện chứ không phải gọi trực tiếp phương thức?" Câu trả lời đơn giản: events và delegates không phải phép màu giải quyết mọi bài toán kiến trúc. Nhưng thiếu chúng thì app hiện đại rất nhanh trở nên "chặt chẽ liên kết" (tightly coupled), khi thay đổi một thành phần lại bắt phải chỉnh nhiều chỗ khác. Dùng events và delegates giúp xây dựng hệ thống linh hoạt, dễ mở rộng và dễ bảo trì.
- Lập trình UI (WinForms, WPF, Xamarin, MAUI): xử lý click, hover, nhập text và các hành động của người dùng.
- Thao tác bất đồng bộ: hoàn thành tải file, nhận dữ liệu từ mạng, trigger timer.
- Kiến trúc plugin và hệ thống mở rộng: cho phép gắn module mới mà không cần tích hợp cứng vào mã chính.
- Hệ thống phát/tín hiệu: thông báo cho nhiều component quan tâm về những sự kiện đã xảy ra (notification, pub/sub).
- Quan sát thay đổi trạng thái (observer): phản ứng khi có tin nhắn mới, dữ liệu thay đổi, cập nhật giao diện.
Giờ vào phần thực tế!
2. Архитектурный скелет
Giả sử bạn làm một console app nhỏ — "Mini-chat" để đào tạo nội bộ trong công ty (hoặc để rèn kỹ năng). Ta có các thực thể: User, Chat và có thể có Bot. Khi user gửi tin, chat phải thông báo cho tất cả người dùng và bot đang kết nối — để họ hiển thị tin hoặc, với bot, tạo phản hồi tự động. Đây là kịch bản cổ điển: publisher phát ra sự kiện (event), các handler đăng ký vào delegate EventHandler/EventHandler<TEventArgs>, và phương thức handler như OnMessageReceived xử lý thông báo.
// Lớp User (subscriber)
public class User
{
public string Name { get; }
public User(string name)
{
Name = name;
}
// Phương thức sẽ xử lý sự kiện
public void OnMessageReceived(object? sender, MessageEventArgs e)
{
Console.WriteLine($"[{Name}] đã thấy tin nhắn mới: {e.MessageText}");
}
}
// Event args
public class MessageEventArgs : EventArgs
{
public string MessageText { get; }
public MessageEventArgs(string text) => MessageText = text;
}
// Lớp ChatRoom (publisher)
public class ChatRoom
{
public event EventHandler<MessageEventArgs>? MessageReceived;
public void SendMessage(string text)
{
// Phát sự kiện (thông báo cho tất cả người đăng ký)
MessageReceived?.Invoke(this, new MessageEventArgs(text));
}
}
Ví dụ sử dụng:
var chat = new ChatRoom();
var user1 = new User("Anton");
var user2 = new User("Maria");
chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;
chat.SendMessage("Chào mọi người! 😊");
Trên console sẽ thấy hai thông báo — cả hai user đều biết có tin nhắn mới.
3. Динамическая подписка и отписка
Trong thực tế user có thể rời chat và không muốn nhận tin nữa. Học cách cho user hủy đăng ký đúng (toán tử -=):
// Trong lớp User có thể thêm phương thức "Rời khỏi chat"
public void Unsubscribe(ChatRoom chat)
{
chat.MessageReceived -= OnMessageReceived;
}
Mở rộng ví dụ:
var chat = new ChatRoom();
var user1 = new User("Anton");
var user2 = new User("Maria");
chat.MessageReceived += user1.OnMessageReceived;
chat.MessageReceived += user2.OnMessageReceived;
chat.SendMessage("Tin đầu tiên");
user2.Unsubscribe(chat); // Maria rời chat
chat.SendMessage("Maria sẽ không thấy tin này nữa");
Đăng ký/hủy đăng ký động xuất hiện khắp nơi: cửa sổ đóng, tab đóng, service tạm thời hủy đăng ký khỏi nguồn sự kiện toàn cục. Đừng quên — subscriber không hủy đăng ký sẽ biến thành zombie, và ứng dụng sẽ bị rò rỉ bộ nhớ!
4. Обработка сообщений и генерация ответа
Giờ thêm bot, phản ứng với mọi tin. Giả sử bot tự nói "Xin chào!" nếu trong tin có từ "bot". Đồng thời xem ví dụ nhiều đăng ký và multicast delegates.
public class Bot
{
public string Name { get; }
public Bot(string name) => Name = name;
public void OnMessageReceived(object? sender, MessageEventArgs e)
{
// Bot phản ứng với từ khóa
if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase))
{
if (sender is ChatRoom chatRoom)
{
Console.WriteLine($"[Bot {Name}]: Xin chào! Mình có thể giúp gì?");
// Bot gửi tin trả lời
chatRoom.SendMessage($"Bot {Name} sẵn sàng giúp bạn.");
}
}
}
}
Sử dụng:
var chat = new ChatRoom();
var user = new User("Yevgeniy");
var bot = new Bot("Trợ lý");
chat.MessageReceived += user.OnMessageReceived;
chat.MessageReceived += bot.OnMessageReceived;
// Yevgeniy gửi tin có từ "bot" để kích hoạt bot
chat.SendMessage("Chào bot, bạn khỏe không?");
Console có thể in ra (thứ tự gọi không được đảm bảo):
[Yevgeniy] đã thấy tin nhắn mới: Chào bot, bạn khỏe không?
[Bot Trợ lý]: Xin chào! Mình có thể giúp gì?
[Yevgeniy] đã thấy tin nhắn mới: Bot Trợ lý sẵn sàng giúp bạn.
[Bot Trợ lý]: Xin chào! Mình có thể giúp gì?
[Yevgeniy] đã thấy tin nhắn mới: Bot Trợ lý sẵn sàng giúp bạn.
[Bot Trợ lý]: Xin chào! Mình có thể giúp gì?
...
Ai thấy có bug tiềm ẩn? Đúng rồi — vòng lặp vô hạn: bot phản ứng với chính tin của nó (lại thấy từ "bot"). Một cách ngăn chặn đơn giản là kiểm tra thêm:
public void OnMessageReceived(object? sender, MessageEventArgs e)
{
// Bot không phản ứng với tin của chính nó
if (e.MessageText.Contains("bot", StringComparison.OrdinalIgnoreCase) &&
!e.MessageText.Contains(Name))
{
if (sender is ChatRoom chatRoom)
{
Console.WriteLine($"[Bot {Name}]: Xin chào! Mình có thể giúp gì?");
chatRoom.SendMessage($"Bot {Name} sẵn sàng giúp bạn.");
}
}
}
Thực tế những tình huống này là lý do để cân nhắc xử lý event vòng lặp, lambda expressions hoặc thậm chí cơ chế hủy tiếp các handler (xem phần sau).
5. Асинхронные операции и колбэки
Rất hay dùng events để báo khi một thao tác bất đồng bộ hoàn thành. Ví dụ tải dữ liệu từ internet hoặc tính toán lâu: event Completed báo kết thúc, và phương thức RunLongOperationAsync chạy việc lâu.
public class LongRunner
{
// Event báo hoàn thành
public event EventHandler<EventArgs>? Completed;
public async Task RunLongOperationAsync()
{
Console.WriteLine("Đang bắt đầu thao tác dài...");
await Task.Delay(2000); // Mô phỏng công việc lâu
Console.WriteLine("Thao tác hoàn thành, thông báo cho các subscriber.");
Completed?.Invoke(this, EventArgs.Empty);
}
}
Mã client:
var runner = new LongRunner();
// Đăng ký vào event hoàn thành
runner.Completed += (sender, e) =>
{
Console.WriteLine("Nhận được thông báo: thao tác đã hoàn thành!");
};
await runner.RunLongOperationAsync();
Đây là nền tảng cho tương tác bất đồng bộ — events thường được dùng trong UI frameworks để báo tải xong, kết thúc animation, click của người dùng, v.v.
6. Сигнальные системы
Xem bài toán kinh điển khác: hệ thống thông báo (ví dụ cửa hàng online có giảm giá). Tất cả "listener" quan tâm biết qua event SaleOccurred:
public class SaleNotifier
{
public event EventHandler<SaleEventArgs>? SaleOccurred;
public void AnnounceSale(string product, decimal newPrice)
{
SaleOccurred?.Invoke(this, new SaleEventArgs(product, newPrice));
}
}
public class SaleEventArgs : EventArgs
{
public string Product { get; }
public decimal NewPrice { get; }
public SaleEventArgs(string product, decimal price)
{
Product = product; NewPrice = price;
}
}
public class Customer
{
public string Name { get; }
public Customer(string name) => Name = name;
public void OnSale(object? sender, SaleEventArgs e)
{
Console.WriteLine($"[{Name}] đã nhận thông báo: {e.Product} giờ có giá {e.NewPrice.ToString("F2")} u.e.!");
}
}
Sử dụng:
var notifier = new SaleNotifier();
var c1 = new Customer("Andrey");
var c2 = new Customer("Olga");
notifier.SaleOccurred += c1.OnSale;
notifier.SaleOccurred += c2.OnSale;
notifier.AnnounceSale("Ấm đun nước", 999.99m);
Nếu một khách không còn quan tâm — hủy đăng ký:
notifier.SaleOccurred -= c2.OnSale;
notifier.AnnounceSale("Máy trộn", 1999.99m);
Pattern này dùng cho gửi tín hiệu (notification, pub/sub), rất quan trọng trong kiến trúc hiện đại.
7. Реализация механизма отмены цепочки событий
Đôi khi một handler cần "dừng" việc thông báo tiếp theo. Thường dùng subclass của EventArgs có cờ hủy. Ví dụ dưới ta duyệt thủ công GetInvocationList() và dừng khi Cancel = true.
public class CancelEventArgs : EventArgs
{
public bool Cancel { get; set; }
}
public class EventSource
{
public event EventHandler<CancelEventArgs>? SomethingHappened;
public void DoSomething()
{
var args = new CancelEventArgs();
// Vòng lặp cổ điển để duyệt các subscriber (nếu cần kiểm soát thứ tự)
var handlers = SomethingHappened?.GetInvocationList();
if (handlers != null)
{
foreach (var handler in handlers)
{
((EventHandler<CancelEventArgs>)handler)(this, args);
if (args.Cancel)
{
Console.WriteLine("Chuỗi thông báo đã bị dừng.");
break;
}
}
}
}
}
Sử dụng:
var source = new EventSource();
source.SomethingHappened += (s, e) =>
{
Console.WriteLine("Bộ xử lý đầu tiên");
};
source.SomethingHappened += (s, e) =>
{
Console.WriteLine("Bộ xử lý thứ hai huỷ sự kiện.");
e.Cancel = true;
};
source.SomethingHappened += (s, e) =>
{
Console.WriteLine("Bộ xử lý này sẽ không được gọi.");
};
source.DoSomething();
Kết quả:
Bộ xử lý đầu tiên
Bộ xử lý thứ hai huỷ sự kiện.
Chuỗi thông báo đã bị dừng.
Cơ chế này hữu ích cho validation, xử lý sự kiện trước khi đóng cửa sổ, kiểm tra quyền và những tình huống cần nói "Dừng! Không xử lý tiếp."
8. Потокобезопасность и управление подписчиками
Trong app đa luồng, khi subscribers có thể được thêm/xóa cùng lúc event được phát, cần dùng pattern thread-safe (xem tài liệu chính thức). Có thể hiện thực event thủ công với accessor add và remove và bảo vệ bằng lock:
public class CustomEvent
{
private EventHandler? _handlers;
public event EventHandler SomethingHappened
{
add
{
lock (this) // An toàn với đa luồng
{
_handlers += value;
}
}
remove
{
lock (this)
{
_handlers -= value;
}
}
}
protected void RaiseEvent()
{
// Dùng bản sao
EventHandler? handler;
lock (this)
{
handler = _handlers;
}
handler?.Invoke(this, EventArgs.Empty);
}
}
Cách này ít gặp (thường cơ chế mặc định là đủ), nhưng đôi khi cần cho hệ thống tải cao.
9. События в UI — из WinForms/WPF/MAUI
WinForms:
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Nút đã được nhấn!");
}
Đằng sau phương thức này là cơ chế đăng ký:
button1.Click += button1_Click;
WPF/MAUI: events kiểu PropertyChanged (thông báo khi thuộc tính model thay đổi):
public class MyViewModel : INotifyPropertyChanged
{
private string _value;
public string Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
Frameworks tận dụng events để xây dựng giao diện phản ứng (MVVM).
10. Типичные ошибки и грабли при работе с событиями
Lỗi #1: tạo rò rỉ bộ nhớ.
Đây là vấn đề thường gặp nhất. Nếu object sống ngắn (ví dụ cửa sổ hoặc service tạm) đăng ký vào event của object sống lâu (ví dụ cache toàn cục hoặc lớp static) mà không hủy khi bị hủy, nó sẽ ở lại trong bộ nhớ mãi. Luôn hủy đăng ký trong Dispose hoặc nơi phù hợp trong vòng đời object.
Lỗi №2: tạo vòng lặp vô hạn giữa các sự kiện.
Subscriber có thể phản ứng bằng hành động lại phát cùng event, dẫn đến đệ quy vô hạn và app crash với StackOverflowException. Luôn kiểm tra điều kiện để handler không phản ứng lại với event chính nó đã gây ra.
Lỗi №3: phụ thuộc vào thứ tự gọi các subscriber.
Đừng bao giờ tin rằng các handler sẽ được gọi theo thứ tự đăng ký. C# spec và CLR không đảm bảo điều này. Nếu thứ tự quan trọng, gọi thủ công các subscriber qua GetInvocationList() theo thứ tự bạn muốn.
Lỗi №4: làm việc không an toàn với events trong môi trường đa luồng.
Nếu subscribers được thêm hoặc xóa từ một thread trong khi event được gọi từ thread khác, có thể xảy ra race condition. Cách bảo vệ cổ điển — copy delegate vào biến cục bộ trước khi gọi:
var handler = MyEvent;
handler?.Invoke(this, EventArgs.Empty);
Hoặc dùng lock khi hiện thực event thủ công qua add/remove.
GO TO FULL VERSION