مقدمه

در درس قبل دیدیم که چگونه می‌توان با استفاده از یک نماینده‌ی (delegate) عمومی، الگوی ناشر-مشترک را برای ارسال اعلان پیاده‌سازی کرد. این الگو بسیار قدرتمند است، اما یک ضعف بزرگ دارد: وقتی یک نماینده به صورت public تعریف می‌شود، هر کدی از خارج کلاس می‌تواند آن را به طور کامل بازنویسی کند (با عملگر `=`)، یا حتی آن را مستقیماً فراخوانی (Invoke) کند. این کار اصل کپسوله‌سازی را نقض می‌کند، زیرا کنترل اعلان‌ها از دست کلاس ناشر خارج می‌شود. برای حل این مشکل، C# یک مکانیزم سطح بالاتر و امن‌تر به نام رویداد (Event) را فراهم کرده است. یک رویداد در واقع یک "پوشش محافظ" در اطراف یک نماینده است که دسترسی به آن را محدود می‌کند.

رویداد (Event) چیست؟

یک رویداد مکانیزمی است که به یک کلاس (ناشر) اجازه می‌دهد تا به دیگر کلاس‌ها (مشترکین) در مورد وقوع یک اتفاق مهم اطلاع‌رسانی کند. مشترکین می‌توانند متدهایی را برای "گوش دادن" به این رویداد ثبت‌نام کنند. وقتی ناشر رویداد را "برمی‌انگیزد" یا "آتش می‌کند" (Raise/Fire)، تمام متدهای ثبت‌نام شده به طور خودکار فراخوانی می‌شوند.

در پشت صحنه، هر رویداد توسط یک نماینده پشتیبانی می‌شود. کلمه‌ی کلیدی event به سادگی دسترسی به آن نماینده را محدود می‌کند:

  • از خارج از کلاس، شما فقط می‌توانید با عملگرهای += (برای اشتراک) و -= (برای لغو اشتراک) با رویداد کار کنید.
  • فقط خود کلاسی که رویداد را تعریف کرده، می‌تواند آن را برانگیزد (فراخوانی کند).

پیاده‌سازی الگو با رویدادها

بیایید مثال FileDownloader از درس قبل را با استفاده از event بازنویسی کنیم تا امنیت و کپسوله‌سازی آن را بهبود بخشیم.

Copy Icon FileDownloader.cs
public class FileDownloader
{
    // Use the built-in Action delegate.
    // The 'event' keyword restricts access to the delegate.
    public event Action<int> OnProgress;

    public void DownloadFile()
    {
        Console.WriteLine("Download started...");
        for (int i = 0; i <= 100; i += 10)
        {
            Thread.Sleep(200);

            // Raise the event from within the class.
            OnProgress?.Invoke(i);
        }
        Console.WriteLine("Download finished.");
    }
}

تنها تغییری که ما ایجاد کردیم، افزودن کلمه‌ی کلیدی event بود. کد داخلی کلاس ناشر دقیقاً مانند قبل است. کد مشترکین نیز برای ثبت‌نام تغییری نمی‌کند:

Copy Icon Program.cs
FileDownloader downloader = new();
downloader.OnProgress += (percent) => Console.WriteLine($"Progress: {percent}%");

اما اکنون، کدهای خارجی دیگر نمی‌توانند کارهای خطرناک زیر را انجام دهند:

Copy Icon Program.cs
// The following lines will now cause COMPILE-TIME ERRORS.

// ERROR: Cannot directly assign to an event.
// downloader.OnProgress = (p) => Console.WriteLine("Hacked!");

// ERROR: Cannot raise the event from outside the class.
// downloader.OnProgress(50);

کلمه‌ی کلیدی event با موفقیت از نماینده‌ی ما محافظت کرده و تضمین می‌کند که فقط خود کلاس FileDownloader کنترل کامل بر روی زمان و چگونگی ارسال اعلان‌ها را دارد.

الگوی استاندارد رویداد در .NET

هرچند استفاده از Action<T> برای رویدادها کاملاً کار می‌کند، اما فریم‌ورک .NET یک الگوی استاندارد و رایج برای تعریف رویدادها دارد که استفاده از آن قویاً توصیه می‌شود. این الگو قابلیت همکاری بهتری با دیگر اجزای فریم‌ورک را فراهم می‌کند.

این الگو بر پایه‌ی نماینده‌ی جنریک System.EventHandler<TEventArgs> است. امضای این نماینده به صورت زیر است:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
  • `sender`: شیئی است که رویداد را برانگیخته است (همان ناشر).
  • `e`: یک شیء است که داده‌های مربوط به رویداد را در خود حمل می‌کند. این شیء باید از کلاس پایه‌ی EventArgs ارث‌بری کند.

برای پیاده‌سازی این الگو، ابتدا یک کلاس سفارشی برای داده‌های رویداد خود می‌سازیم:

Copy Icon Program.cs
// 1. Create a custom class for event data, inheriting from EventArgs.
public class ProgressEventArgs : EventArgs
{
    public int Percentage { get; }
    public ProgressEventArgs(int percentage) { Percentage = percentage; }
}

public class StandardFileDownloader
{
    // 2. Declare the event using the standard EventHandler delegate.
    public event EventHandler<ProgressEventArgs> OnProgress;

    public void DownloadFile()
    {
        for (int i = 0; i <= 100; i += 25)
        {
            // 3. Raise the event, passing 'this' as the sender and a new EventArgs object.
            OnProgress?.Invoke(this, new ProgressEventArgs(i));
        }
    }
}

استفاده از این الگو باعث می‌شود کد شما برای دیگر برنامه‌نویسان C# بسیار آشناتر و قابل فهم‌تر باشد.