CodeGym /Các khóa học /C# SELF /Kịch bản thực tế và best practices

Kịch bản thực tế và best practices

C# SELF
Mức độ , Bài học
Có sẵn

1. Kịch bản thực tế dùng events

Events không phải chỉ là lý thuyết đẹp trong sách. Thực tế chúng cần thiết trong gần như mọi app, và nếu bạn làm với UI hoặc dịch vụ mạng — thì hầu hết trường hợp sẽ dùng.

Ta xem vài kịch bản điển hình từ dev thực tế. Đồng thời xem cách các chuẩn mực và best practices giúp tránh lỗi phổ biến.

Phản ứng với hành động người dùng (như UI)

Có lẽ kịch bản hay gặp nhất — khi người dùng làm gì đó (nhấn nút, chọn mục trong list), và app phải phản hồi. Đó là cách hoạt động của Windows Forms, WPF, UWP, Avalonia, MAUI và các UI framework khác.

Ví dụ nhỏ (không có UI — giả lập nút):


public class Button
{
    public event EventHandler? Click; // delegate chuẩn

    public void SimulateClick()
    {
        Click?.Invoke(this, EventArgs.Empty); // "Nút" đã "được nhấn"
    }
}

class Program
{
    static void Main()
    {
        var button = new Button();
        button.Click += (sender, e) => Console.WriteLine("Đã nhấn nút!");
        button.SimulateClick(); // > Đã nhấn nút!
    }
}

Trong một UI framework thực, event Click được kích hoạt khi người dùng bấm chuột lên nút hoặc chạm trên màn hình. Mọi thứ subscribe vào event đó sẽ nhận thông báo — đó là cách logic app vận hành.

Thông báo tiến trình, kết thúc thao tác và lỗi

Tưởng tượng: tải file, xử lý dữ liệu, tính toán lâu. Cần báo tiến trình hoặc lỗi. Thường tổ chức event cho "bước tiến trình" và event riêng cho "hoàn thành" hoặc "lỗi".

Ví dụ downloader file:


public class FileDownloader
{
    public event EventHandler<int>? ProgressChanged;
    public event EventHandler? DownloadCompleted;
    public event EventHandler<string>? DownloadFailed;

    public void Download()
    {
        for (int i = 1; i <= 100; i += 10)
        {
            Thread.Sleep(50); // giả lập độ trễ
            ProgressChanged?.Invoke(this, i);
        }
        DownloadCompleted?.Invoke(this, EventArgs.Empty);
    }
}

var downloader = new FileDownloader();
downloader.ProgressChanged += (s, progress) => Console.WriteLine($"Đang tải: {progress}%");
downloader.DownloadCompleted += (s, e) => Console.WriteLine("Tải xong.");
downloader.Download();

Ở đây có vài event: bạn có thể đăng ký tất cả (hoặc chỉ cái cần). Giống cách nhiều lớp .NET chuẩn làm với thread, file download, HTTP, v.v.

Phản hồi vòng đời object (ví dụ, "đã lưu", "đã xóa")

Giả sử bạn có domain model. Muốn khi user lưu hoặc xóa object thì một số process phản ứng: cập nhật cache, ghi log, gửi message, v.v.


public class UserRepository
{
    public event EventHandler<UserEventArgs>? UserSaved;
    public event EventHandler<UserEventArgs>? UserDeleted;

    public void Save(User user)
    {
        //... lưu user
        UserSaved?.Invoke(this, new UserEventArgs(user));
    }

    public void Delete(User user)
    {
        //... xóa user
        UserDeleted?.Invoke(this, new UserEventArgs(user));
    }
}

public class UserEventArgs : EventArgs
{
    public User User { get; }
    public UserEventArgs(User user) => User = user;
}

Bây giờ các module khác có thể subscribe và — mà không cần liên kết trực tiếp! — nhận thông báo về mọi thay đổi của user. Repository thậm chí không biết ai đã "bắt".

Sự kiện bất đồng bộ và đa luồng

Đôi khi events được sinh ra không từ thread chính (UI) — ví dụ từ task nền, timer hoặc thao tác async. Khi đó cần nhớ rằng handler sẽ chạy trên thread "ngoài". Nếu handler cố cập nhật UI trực tiếp từ thread khác — sẽ gây lỗi.

Marshaling là gì? Marshaling là chuyển việc thực thi code từ thread này sang thread khác, thường từ thread nền về UI-thread để update giao diện an toàn. Trong các app UI (WinForms, WPF) dùng cơ chế như SynchronizationContext hoặc Dispatcher để "chuyển" cuộc gọi handler về thread mong muốn.

Đừng làm thế này:


// Event được gọi từ thread nền, mà handler cập nhật UI trực tiếp — sẽ ném exception!

Nên làm: kiểm tra thread đang chạy và nếu cần thì marshal về UI-thread (qua SynchronizationContext hoặc Dispatcher). Ở ứng dụng console hoặc server thường không có giới hạn này.

Event Aggregator / Messaging

Trong ứng dụng lớn thường dùng một centralized event aggregator (Event Aggregator). Nó giảm coupling: subscribers và publishers không cần biết nhau, trao đổi thông qua trung tâm.


public class EventAggregator
{
    public event EventHandler<SomeEventArgs>? SomeEvent;

    public void Publish(SomeEventArgs args)
    {
        SomeEvent?.Invoke(this, args);
    }
}

2. Best patterns và thực hành

Dùng attribute [CallerMemberName] cho logging/lỗi

Nếu viết infra code (ví dụ logger, tracer), log tên method nguồn event bằng attribute CallerMemberName — giúp debug dễ hơn.

Cố gắng làm events thread-safe khi cần

Events là multicast, và truy cập từ nhiều thread cần cẩn trọng: trước khi gọi hãy copy delegate vào biến local.


var handler = MyEvent;
if (handler != null) handler(this, args);

Trong C# 6+ dùng gọi an toàn: MyEvent?.Invoke(this, args).

Thiết kế custom EventArgs immutable

Định nghĩa type EventArgs của bạn chỉ với property chỉ đọc — tránh tình trạng subscribers vô tình thay đổi state của event.


public class WorkCompletedEventArgs : EventArgs
{
    public string Message { get; }
    public WorkCompletedEventArgs(string message) => Message = message;
}

public hay private event

Hãy để events là public event, còn việc gọi event thì đóng gói trong protected virtual method OnEventName. Điều này cho phép mở rộng (derived class override), và bên ngoài không thể gọi event trực tiếp.

Đừng phá vỡ thứ tự vòng đời

Nếu bạn khởi tạo một chuỗi handler dài, nhớ rằng ai đó có thể subscribe và cố thay đổi logic nội bộ. Document nơi và khi nào events được gọi, và liệu event có thể chạy nhiều lần hay không.

Nhiều event và composition

Khi một object lắng nghe nhiều nguồn, gom việc subscribe/unsubscribe vào một chỗ (ví dụ methods Subscribe/Unsubscribe). Nếu cần, giữ các field delegate private để dễ unsubscribe.

3. Cách KHÔNG nên dùng events: lỗi thường gặp

Lỗi #1: bỏ qua event pattern chuẩn.
Nếu mỗi lớp tự định nghĩa delegate riêng cho từng event, code sẽ nhanh chóng bừa bộn. Cố dùng delegate chuẩn EventHandler hoặc EventHandler<T>, với T kế thừa EventArgs. Cách này làm code dễ hiểu và quen thuộc với dev .NET.

Lỗi #2: gọi event không đúng từ code bên ngoài.
Không bao giờ gọi event trực tiếp từ ngoài publisher. Event được khởi xướng chỉ bên trong class, thường qua phương thức protected OnEventName. Subscribers chỉ có quyền đăng ký (+=) và hủy đăng ký (-=).

Tệ:


foo.MyEvent(); // sai! Không được gọi event trực tiếp từ ngoài

Đúng:


// Chỉ bên trong foo:
// protected virtual void OnMyEvent()
// {
//     MyEvent?.Invoke(this, EventArgs.Empty);
// }

Lỗi #3: gọi event mà không kiểm tra có subscriber (null).
Nếu cố gọi event mà không có ai subscribe sẽ gặp NullReferenceException. Trong C# 6.0+ dùng ?.Invoke(...) — event chỉ được gọi khi có subscriber.

Lỗi #4: quên unsubscribe (memory leak).
Nếu object subscribe vào event mà không unsubscribe trước khi bị hủy, publisher giữ reference tới subscriber. GC không thể thu hồi object đó — vấn đề nghiêm trọng khi publisher sống lâu còn subscriber sống ngắn (ví dụ ViewModel, form). Giải pháp tốt nhất là unsubscribe rõ ràng hoặc dùng weak references (WeakReference) khi phù hợp.

Lỗi #5: gọi event ngoài protected virtual method OnEvent.
Gọi event trực tiếp sẽ tước đi cơ hội cho class kế thừa override hành vi. Pattern đúng là định nghĩa protected virtual method gọi event.

Ví dụ chuẩn:


protected virtual void OnSomething(EventArgs e)
{
    Something?.Invoke(this, e);
}
1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Vòng đời của sự kiện
Phân tích chi tiết về việc đăng ký sự kiện
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION